mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-22 13:50:49 +01:00
Merge e56a563286
into 9ca7d24f6a
This commit is contained in:
commit
188386098e
24 changed files with 1885 additions and 23 deletions
|
@ -59,6 +59,7 @@ Here are some highlights of go-mail's featureset:
|
||||||
* [X] Custom error types for delivery errors
|
* [X] Custom error types for delivery errors
|
||||||
* [X] Custom dial-context functions for more control over the connection (proxing, DNS hooking, etc.)
|
* [X] Custom dial-context functions for more control over the connection (proxing, DNS hooking, etc.)
|
||||||
* [X] Output a go-mail message as EML file and parse EML file into a go-mail message
|
* [X] Output a go-mail message as EML file and parse EML file into a go-mail message
|
||||||
|
* [X] S/MIME singed messages
|
||||||
|
|
||||||
go-mail works like a programatic email client and provides lots of methods and functionalities you would consider
|
go-mail works like a programatic email client and provides lots of methods and functionalities you would consider
|
||||||
standard in a MUA.
|
standard in a MUA.
|
||||||
|
|
26
dummy-chain-cert-ecdsa.pem
Normal file
26
dummy-chain-cert-ecdsa.pem
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIICAzCCAaqgAwIBAgIUKNWGvPrlzuYHnP4m6nGe60LalYEwCgYIKoZIzj0EAwIw
|
||||||
|
ZTELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5MRcw
|
||||||
|
FQYDVQQKDA5JbnRlcm1lZGlhdGVDQTEeMBwGA1UEAwwVSW50ZXJtZWRpYXRlIEVD
|
||||||
|
RFNBIENBMB4XDTI0MTAxNzEzMDg0N1oXDTI2MTAxNzEzMDg0N1owWzELMAkGA1UE
|
||||||
|
BhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5MRIwEAYDVQQKDAlF
|
||||||
|
bmRFbnRpdHkxGTAXBgNVBAMMEEVuZCBFbnRpdHkgRUNEU0EwWTATBgcqhkjOPQIB
|
||||||
|
BggqhkjOPQMBBwNCAATMkQg2RhaDYOWs+Ik/NKgZdwxm4BzagwVou6R73uiwXsRi
|
||||||
|
QgJfhBxp01S3JI4E8AiboFf4uPRnRh0HGl9cUR+po0IwQDAdBgNVHQ4EFgQUz247
|
||||||
|
eAeMuM2We19rA5HnLzyLZEUwHwYDVR0jBBgwFoAU6oYLh690kT1bIB3DUA/SGRim
|
||||||
|
dXswCgYIKoZIzj0EAwIDRwAwRAIgI7cIpGzoiU1IoTYniEGXtK+WXq4Luv8k3BJQ
|
||||||
|
W16RAVsCICnuLaRyH/5nA3mmciAiF5R9PKDzyBnJcPGuCM1tmEpN
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIB/zCCAaSgAwIBAgIURGCUhG09dwZ2DwtWHjQxGTnNT3UwCgYIKoZIzj0EAwIw
|
||||||
|
VTELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBVN0YXRlMQ0wCwYDVQQHDARDaXR5MQ8w
|
||||||
|
DQYDVQQKDAZSb290Q0ExFjAUBgNVBAMMDVJvb3QgRUNEU0EgQ0EwHhcNMjQxMDE3
|
||||||
|
MTMwODMwWhcNMjkxMDE2MTMwODMwWjBlMQswCQYDVQQGEwJVUzEOMAwGA1UECAwF
|
||||||
|
U3RhdGUxDTALBgNVBAcMBENpdHkxFzAVBgNVBAoMDkludGVybWVkaWF0ZUNBMR4w
|
||||||
|
HAYDVQQDDBVJbnRlcm1lZGlhdGUgRUNEU0EgQ0EwWTATBgcqhkjOPQIBBggqhkjO
|
||||||
|
PQMBBwNCAASYjGGvlMFZwnqUc8+jt9I7qRT8IP5gYLPZ7oiV/oaGNinmtzG7UXC/
|
||||||
|
2PEDvdqpMPNw65IaP0d8z+c5lUxneE70o0IwQDAdBgNVHQ4EFgQU6oYLh690kT1b
|
||||||
|
IB3DUA/SGRimdXswHwYDVR0jBBgwFoAUGVvIlDmd/tCejujJcy4xTvgkw3IwCgYI
|
||||||
|
KoZIzj0EAwIDSQAwRgIhAO0rw5JvMu5y8JO958/4FThdjwOsg/IDGryQ3QQM0tw1
|
||||||
|
AiEA07W1o81WIOVLZzwyTJAN8SnpSRIXgV/+ccst2T7s7Zs=
|
||||||
|
-----END CERTIFICATE-----
|
3
dummy-chain-cert-ecdsa.pem.license
Normal file
3
dummy-chain-cert-ecdsa.pem.license
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
62
dummy-chain-cert-rsa.pem
Normal file
62
dummy-chain-cert-rsa.pem
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
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
|
||||||
|
-----END CERTIFICATE-----
|
3
dummy-chain-cert-rsa.pem.license
Normal file
3
dummy-chain-cert-rsa.pem.license
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
8
dummy-child-key-ecdsa.pem
Normal file
8
dummy-child-key-ecdsa.pem
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
-----BEGIN EC PARAMETERS-----
|
||||||
|
BggqhkjOPQMBBw==
|
||||||
|
-----END EC PARAMETERS-----
|
||||||
|
-----BEGIN EC PRIVATE KEY-----
|
||||||
|
MHcCAQEEIG20X4lI7vocwK2W3TYOW+XtQv/bwRWYknvAf2OK8WFJoAoGCCqGSM49
|
||||||
|
AwEHoUQDQgAEzJEINkYWg2DlrPiJPzSoGXcMZuAc2oMFaLuke97osF7EYkICX4Qc
|
||||||
|
adNUtySOBPAIm6BX+Lj0Z0YdBxpfXFEfqQ==
|
||||||
|
-----END EC PRIVATE KEY-----
|
3
dummy-child-key-ecdsa.pem.license
Normal file
3
dummy-child-key-ecdsa.pem.license
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
52
dummy-child-key-rsa.pem
Normal file
52
dummy-child-key-rsa.pem
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
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
|
||||||
|
S++xGnGHEUSEl7cWnO8k0RyJzzc=
|
||||||
|
-----END PRIVATE KEY-----
|
3
dummy-child-key-rsa.pem.license
Normal file
3
dummy-child-key-rsa.pem.license
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
12
encoding.go
12
encoding.go
|
@ -171,17 +171,20 @@ const (
|
||||||
|
|
||||||
// TypeTextPlain represents the MIME type for plain text content.
|
// TypeTextPlain represents the MIME type for plain text content.
|
||||||
TypeTextPlain ContentType = "text/plain"
|
TypeTextPlain ContentType = "text/plain"
|
||||||
|
|
||||||
|
// typeSMimeSigned represents the MIME type for S/MIME singed messages.
|
||||||
|
typeSMimeSigned ContentType = `application/pkcs7-signature; name="smime.p7s"`
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// MIMEAlternative MIMEType represents a MIME multipart/alternative type, used for emails with multiple versions.
|
// 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 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 represents a MIME multipart/related type, used for emails with related content entities.
|
||||||
MIMERelated MIMEType = "related"
|
MIMERelated MIMEType = "related"
|
||||||
|
// MIMESMime MIMEType represents a MIME multipart/signed type, used for siging emails with S/MIME.
|
||||||
|
MIMESMime MIMEType = `signed; protocol="application/pkcs7-signature"; micalg=sha-256`
|
||||||
)
|
)
|
||||||
|
|
||||||
// String satisfies the fmt.Stringer interface for the Charset type.
|
// String satisfies the fmt.Stringer interface for the Charset type.
|
||||||
|
@ -219,3 +222,8 @@ func (c ContentType) String() string {
|
||||||
func (e Encoding) String() string {
|
func (e Encoding) String() string {
|
||||||
return string(e)
|
return string(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// String is a standard method to convert an MIMEType into a printable format
|
||||||
|
func (e MIMEType) String() string {
|
||||||
|
return string(e)
|
||||||
|
}
|
||||||
|
|
|
@ -61,6 +61,11 @@ func TestContentType_String(t *testing.T) {
|
||||||
"ContentType: application/pgp-encrypted", TypePGPEncrypted,
|
"ContentType: application/pgp-encrypted", TypePGPEncrypted,
|
||||||
"application/pgp-encrypted",
|
"application/pgp-encrypted",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"ContentType: pkcs7-signature", typeSMimeSigned,
|
||||||
|
`application/pkcs7-signature; name="smime.p7s"`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
@ -121,3 +126,24 @@ func TestCharset_String(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestContentType_String tests the mime type method of the MIMEType object
|
||||||
|
func TestMimeType_String(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
mt MIMEType
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{MIMEAlternative, "alternative"},
|
||||||
|
{MIMEMixed, "mixed"},
|
||||||
|
{MIMERelated, "related"},
|
||||||
|
{MIMESMime, `signed; protocol="application/pkcs7-signature"; micalg=sha-256`},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.mt.String(), func(t *testing.T) {
|
||||||
|
if tt.mt.String() != tt.want {
|
||||||
|
t.Errorf("wrong string for mime type returned. Expected: %s, got: %s",
|
||||||
|
tt.want, tt.mt.String())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
132
msg.go
132
msg.go
|
@ -7,6 +7,10 @@ package mail
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
"embed"
|
"embed"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -24,7 +28,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// ErrNoFromAddress indicates that the FROM address is not set, which is required.
|
// ErrNoFromAddress should be used when a FROM address is requested but not set
|
||||||
ErrNoFromAddress = errors.New("no FROM address set")
|
ErrNoFromAddress = errors.New("no FROM address set")
|
||||||
|
|
||||||
// ErrNoRcptAddresses indicates that no recipient addresses have been set.
|
// ErrNoRcptAddresses indicates that no recipient addresses have been set.
|
||||||
|
@ -144,6 +148,9 @@ type Msg struct {
|
||||||
//
|
//
|
||||||
// This can be useful in scenarios where headers are conditionally passed based on receipt - i. e. SMTP proxies.
|
// This can be useful in scenarios where headers are conditionally passed based on receipt - i. e. SMTP proxies.
|
||||||
noDefaultUserAgent bool
|
noDefaultUserAgent bool
|
||||||
|
|
||||||
|
// SMime represents a middleware used to sign messages with S/MIME
|
||||||
|
sMime *SMime
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendmailPath is the default system path to the sendmail binary - at least on standard Unix-like OS.
|
// SendmailPath is the default system path to the sendmail binary - at least on standard Unix-like OS.
|
||||||
|
@ -333,8 +340,72 @@ func WithNoDefaultUserAgent() MsgOption {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetCharset sets or overrides the currently set encoding charset of the Msg.
|
// SignWithSMimeRSA configures the Msg to be signed with S/MIME
|
||||||
//
|
func (m *Msg) SignWithSMimeRSA(privateKey *rsa.PrivateKey, certificate *x509.Certificate, intermediateCertificate *x509.Certificate) error {
|
||||||
|
sMime, err := newSMimeWithRSA(privateKey, certificate, intermediateCertificate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.sMime = sMime
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignWithSMimeECDSA configures the Msg to be signed with S/MIME
|
||||||
|
func (m *Msg) SignWithSMimeECDSA(privateKey *ecdsa.PrivateKey, certificate *x509.Certificate, intermediateCertificate *x509.Certificate) error {
|
||||||
|
sMime, err := newSMimeWithECDSA(privateKey, certificate, intermediateCertificate)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.sMime = sMime
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignWithTLSCertificate signs the Msg with the provided *tls.certificate.
|
||||||
|
func (m *Msg) SignWithTLSCertificate(keyPairTlS *tls.Certificate) error {
|
||||||
|
intermediateCertificate, err := x509.ParseCertificate(keyPairTlS.Certificate[1])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to parse intermediate certificate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
leafCertificate, err := getLeafCertificate(keyPairTlS)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get leaf certificate: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch keyPairTlS.PrivateKey.(type) {
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
if intermediateCertificate == nil {
|
||||||
|
return m.SignWithSMimeRSA(keyPairTlS.PrivateKey.(*rsa.PrivateKey), leafCertificate, nil)
|
||||||
|
}
|
||||||
|
return m.SignWithSMimeRSA(keyPairTlS.PrivateKey.(*rsa.PrivateKey), leafCertificate, intermediateCertificate)
|
||||||
|
|
||||||
|
case *ecdsa.PrivateKey:
|
||||||
|
if intermediateCertificate == nil {
|
||||||
|
return m.SignWithSMimeECDSA(keyPairTlS.PrivateKey.(*ecdsa.PrivateKey), leafCertificate, nil)
|
||||||
|
}
|
||||||
|
return m.SignWithSMimeECDSA(keyPairTlS.PrivateKey.(*ecdsa.PrivateKey), leafCertificate, intermediateCertificate)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported private key type: %T", keyPairTlS.PrivateKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getLeafCertificate returns the leaf certificate from a tls.Certificate.
|
||||||
|
// PLEASE NOTE: Before Go 1.23 Certificate.Leaf was left nil, and the parsed certificate was
|
||||||
|
// discarded. This behavior can be re-enabled by setting "x509keypairleaf=0"
|
||||||
|
// in the GODEBUG environment variable.
|
||||||
|
func getLeafCertificate(keyPairTlS *tls.Certificate) (*x509.Certificate, error) {
|
||||||
|
if keyPairTlS.Leaf != nil {
|
||||||
|
return keyPairTlS.Leaf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := x509.ParseCertificate(keyPairTlS.Certificate[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cert, nil
|
||||||
|
}
|
||||||
|
|
||||||
// This method allows you to specify a character set for the email message. The charset is
|
// This method allows you to specify a character set for the email message. The charset is
|
||||||
// important for ensuring that the content of the message is correctly interpreted by
|
// important for ensuring that the content of the message is correctly interpreted by
|
||||||
// mail clients. Common charset values include UTF-8, ISO-8859-1, and others. If a charset
|
// mail clients. Common charset values include UTF-8, ISO-8859-1, and others. If a charset
|
||||||
|
@ -2152,6 +2223,41 @@ func (m *Msg) applyMiddlewares(msg *Msg) *Msg {
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// signMessage sign the Msg with S/MIME
|
||||||
|
func (m *Msg) signMessage(msg *Msg) (*Msg, error) {
|
||||||
|
signedPart := msg.GetParts()[0]
|
||||||
|
body, err := signedPart.GetContent()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
signaturePart, err := m.createSignaturePart(signedPart.GetEncoding(), signedPart.GetContentType(), signedPart.GetCharset(), body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
m.parts = append(m.parts, signaturePart)
|
||||||
|
|
||||||
|
return m, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// createSignaturePart creates an additional part that be used for storing the S/MIME signature of the given body
|
||||||
|
func (m *Msg) createSignaturePart(encoding Encoding, contentType ContentType, charSet Charset, body []byte) (*Part, error) {
|
||||||
|
message, err := m.sMime.prepareMessage(encoding, contentType, charSet, body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
signedMessage, err := m.sMime.signMessage(*message)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
signaturePart := m.newPart(typeSMimeSigned, WithPartEncoding(EncodingB64), WithSMimeSinging())
|
||||||
|
signaturePart.SetContent(*signedMessage)
|
||||||
|
|
||||||
|
return signaturePart, nil
|
||||||
|
}
|
||||||
|
|
||||||
// WriteTo writes the formatted Msg into the given io.Writer and satisfies the io.WriterTo interface.
|
// WriteTo writes the formatted Msg into the given io.Writer and satisfies the io.WriterTo interface.
|
||||||
//
|
//
|
||||||
// This method writes the email message, including its headers, body, and attachments, to the provided
|
// This method writes the email message, including its headers, body, and attachments, to the provided
|
||||||
|
@ -2169,7 +2275,18 @@ func (m *Msg) applyMiddlewares(msg *Msg) *Msg {
|
||||||
// - https://datatracker.ietf.org/doc/html/rfc5322
|
// - https://datatracker.ietf.org/doc/html/rfc5322
|
||||||
func (m *Msg) WriteTo(writer io.Writer) (int64, error) {
|
func (m *Msg) WriteTo(writer io.Writer) (int64, error) {
|
||||||
mw := &msgWriter{writer: writer, charset: m.charset, encoder: m.encoder}
|
mw := &msgWriter{writer: writer, charset: m.charset, encoder: m.encoder}
|
||||||
mw.writeMsg(m.applyMiddlewares(m))
|
msg := m.applyMiddlewares(m)
|
||||||
|
|
||||||
|
if m.hasSMime() {
|
||||||
|
signedMsg, err := m.signMessage(msg)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
msg = signedMsg
|
||||||
|
}
|
||||||
|
|
||||||
|
mw.writeMsg(msg)
|
||||||
|
|
||||||
return mw.bytesWritten, mw.err
|
return mw.bytesWritten, mw.err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2533,7 +2650,7 @@ func (m *Msg) hasAlt() bool {
|
||||||
count++
|
count++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return count > 1 && m.pgptype == 0
|
return count > 1 && m.pgptype == 0 && !m.hasSMime()
|
||||||
}
|
}
|
||||||
|
|
||||||
// hasMixed returns true if the Msg has mixed parts.
|
// hasMixed returns true if the Msg has mixed parts.
|
||||||
|
@ -2551,6 +2668,11 @@ func (m *Msg) hasMixed() bool {
|
||||||
return m.pgptype == 0 && ((len(m.parts) > 0 && len(m.attachments) > 0) || len(m.attachments) > 1)
|
return m.pgptype == 0 && ((len(m.parts) > 0 && len(m.attachments) > 0) || len(m.attachments) > 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// hasSMime returns true if the Msg has should be signed with S/MIME
|
||||||
|
func (m *Msg) hasSMime() bool {
|
||||||
|
return m.sMime != nil
|
||||||
|
}
|
||||||
|
|
||||||
// hasRelated returns true if the Msg has related parts.
|
// hasRelated returns true if the Msg has related parts.
|
||||||
//
|
//
|
||||||
// This method checks whether the message contains related parts, such as inline embedded files
|
// This method checks whether the message contains related parts, such as inline embedded files
|
||||||
|
|
295
msg_test.go
295
msg_test.go
|
@ -6277,6 +6277,39 @@ func TestMsg_hasAlt(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestMsg_hasAlt tests the hasAlt() method of the Msg with active S/MIME
|
||||||
|
func TestMsg_hasAltWithSMime(t *testing.T) {
|
||||||
|
privateKey, certificate, intermediateCertificate, err := getDummyRSACryptoMaterial()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to laod dummy crypto material. Cause: %v", err)
|
||||||
|
}
|
||||||
|
m := NewMsg()
|
||||||
|
m.SetBodyString(TypeTextPlain, "Plain")
|
||||||
|
m.AddAlternativeString(TypeTextHTML, "<b>HTML</b>")
|
||||||
|
if err := m.SignWithSMimeRSA(privateKey, certificate, intermediateCertificate); err != nil {
|
||||||
|
t.Errorf("failed to init smime configuration")
|
||||||
|
}
|
||||||
|
if m.hasAlt() {
|
||||||
|
t.Errorf("mail has alternative parts and S/MIME is active, but hasAlt() returned true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMsg_hasSMime tests the hasSMime() method of the Msg
|
||||||
|
func TestMsg_hasSMime(t *testing.T) {
|
||||||
|
privateKey, certificate, intermediateCertificate, err := getDummyRSACryptoMaterial()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to laod dummy crypto material. Cause: %v", err)
|
||||||
|
}
|
||||||
|
m := NewMsg()
|
||||||
|
if err := m.SignWithSMimeRSA(privateKey, certificate, intermediateCertificate); err != nil {
|
||||||
|
t.Errorf("failed to init smime configuration")
|
||||||
|
}
|
||||||
|
m.SetBodyString(TypeTextPlain, "Plain")
|
||||||
|
if !m.hasSMime() {
|
||||||
|
t.Errorf("mail has smime configured but hasSMime() returned true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMsg_hasMixed(t *testing.T) {
|
func TestMsg_hasMixed(t *testing.T) {
|
||||||
t.Run("message has no mixed", func(t *testing.T) {
|
t.Run("message has no mixed", func(t *testing.T) {
|
||||||
message := testMessage(t)
|
message := testMessage(t)
|
||||||
|
@ -6353,6 +6386,70 @@ func TestMsg_hasRelated(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestMsg_WriteToWithSMIME tests the WriteTo() method of the Msg
|
||||||
|
func TestMsg_WriteToWithSMIME(t *testing.T) {
|
||||||
|
privateKey, certificate, intermediateCertificate, err := getDummyRSACryptoMaterial()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to laod dummy crypto material. Cause: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := NewMsg()
|
||||||
|
m.Subject("This is a test")
|
||||||
|
m.SetBodyString(TypeTextPlain, "Plain")
|
||||||
|
if err := m.SignWithSMimeRSA(privateKey, certificate, intermediateCertificate); err != nil {
|
||||||
|
t.Errorf("failed to init smime configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
wbuf := bytes.Buffer{}
|
||||||
|
if _, err = m.WriteTo(&wbuf); err != nil {
|
||||||
|
t.Errorf("WriteTo() failed: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := wbuf.String()
|
||||||
|
boundary := result[strings.LastIndex(result, "--")-60 : strings.LastIndex(result, "--")]
|
||||||
|
if strings.Count(result, boundary) != 4 {
|
||||||
|
t.Errorf("WriteTo() failed. False number of boundaries found")
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(result, fmt.Sprintf("--%s", boundary))
|
||||||
|
if len(parts) != 4 {
|
||||||
|
t.Errorf("WriteTo() failed. False number of parts found")
|
||||||
|
}
|
||||||
|
|
||||||
|
preamble := parts[0]
|
||||||
|
if !strings.Contains(preamble, "MIME-Version: 1.0") {
|
||||||
|
t.Errorf("WriteTo() failed. Unable to find MIME-Version")
|
||||||
|
}
|
||||||
|
if !strings.Contains(preamble, "Subject: This is a test") {
|
||||||
|
t.Errorf("WriteTo() failed. Unable to find subject")
|
||||||
|
}
|
||||||
|
if !strings.Contains(preamble, fmt.Sprintf("Content-Type: multipart/signed; protocol=\"application/pkcs7-signature\"; micalg=sha-256;\r\n boundary=%s", boundary)) {
|
||||||
|
t.Errorf("WriteTo() failed. Unable to find Content-Type")
|
||||||
|
}
|
||||||
|
|
||||||
|
signedData := parts[1]
|
||||||
|
if !strings.Contains(signedData, "Content-Transfer-Encoding: quoted-printable") {
|
||||||
|
t.Errorf("WriteTo() failed. Unable to find Content-Transfer-Encoding")
|
||||||
|
}
|
||||||
|
if !strings.Contains(signedData, "Content-Type: text/plain; charset=UTF-8") {
|
||||||
|
t.Errorf("WriteTo() failed. Unable to find Content-Type")
|
||||||
|
}
|
||||||
|
if !strings.Contains(signedData, "Plain") {
|
||||||
|
t.Errorf("WriteTo() failed. Unable to find Content")
|
||||||
|
}
|
||||||
|
|
||||||
|
signature := parts[2]
|
||||||
|
if !strings.Contains(signature, "Content-Transfer-Encoding: base64") {
|
||||||
|
t.Errorf("WriteTo() failed. Unable to find Content-Transfer-Encoding")
|
||||||
|
}
|
||||||
|
if !strings.Contains(signature, `application/pkcs7-signature; name="smime.p7s"`) {
|
||||||
|
t.Errorf("WriteTo() failed. Unable to find Content-Type")
|
||||||
|
}
|
||||||
|
if strings.Contains(signature, "Plain") {
|
||||||
|
t.Errorf("WriteTo() failed. Unable to find signature")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestMsg_hasPGPType(t *testing.T) {
|
func TestMsg_hasPGPType(t *testing.T) {
|
||||||
t.Run("message has no pgpType", func(t *testing.T) {
|
t.Run("message has no pgpType", func(t *testing.T) {
|
||||||
message := testMessage(t)
|
message := testMessage(t)
|
||||||
|
@ -6440,6 +6537,204 @@ func TestMsg_addDefaultHeader(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestSignWithSMime_ValidRSAKeyPair tests WithSMimeSinging with given rsa key pair
|
||||||
|
func TestSignWithSMime_ValidRSAKeyPair(t *testing.T) {
|
||||||
|
privateKey, certificate, intermediateCertificate, err := getDummyRSACryptoMaterial()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to laod dummy crypto material. Cause: %v", err)
|
||||||
|
}
|
||||||
|
m := NewMsg()
|
||||||
|
if err := m.SignWithSMimeRSA(privateKey, certificate, intermediateCertificate); err != nil {
|
||||||
|
t.Errorf("failed to set sMime. Cause: %v", err)
|
||||||
|
}
|
||||||
|
if m.sMime.privateKey.rsa == nil {
|
||||||
|
t.Errorf("WithSMimeSinging() - no private key is given")
|
||||||
|
}
|
||||||
|
if m.sMime.certificate == nil {
|
||||||
|
t.Errorf("WithSMimeSinging() - no certificate is given")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSignWithSMime_ValidRSAKeyPair tests WithSMimeSinging with given ecdsa key pair
|
||||||
|
func TestSignWithSMime_ValidECDSAKeyPair(t *testing.T) {
|
||||||
|
privateKey, certificate, intermediateCertificate, err := getDummyECDSACryptoMaterial()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to laod dummy crypto material. Cause: %v", err)
|
||||||
|
}
|
||||||
|
m := NewMsg()
|
||||||
|
if err := m.SignWithSMimeECDSA(privateKey, certificate, intermediateCertificate); err != nil {
|
||||||
|
t.Errorf("failed to set sMime. Cause: %v", err)
|
||||||
|
}
|
||||||
|
if m.sMime.privateKey.ecdsa == nil {
|
||||||
|
t.Errorf("WithSMimeSinging() - no private key is given")
|
||||||
|
}
|
||||||
|
if m.sMime.certificate == nil {
|
||||||
|
t.Errorf("WithSMimeSinging() - no certificate is given")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSignWithTLSCertificate tests SignWithTLSCertificate with given *tls.Certificate
|
||||||
|
func TestSignWithTLSCertificate(t *testing.T) {
|
||||||
|
keyPairTLS, err := getDummyKeyPairTLS()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to laod dummy crypto material. Cause: %v", err)
|
||||||
|
}
|
||||||
|
m := NewMsg()
|
||||||
|
if err := m.SignWithTLSCertificate(keyPairTLS); err != nil {
|
||||||
|
t.Errorf("failed to set sMime. Cause: %v", err)
|
||||||
|
}
|
||||||
|
if m.sMime.privateKey.ecdsa == nil {
|
||||||
|
t.Errorf("SignWithTLSCertificate() - no private key is given")
|
||||||
|
}
|
||||||
|
if m.sMime.certificate == nil {
|
||||||
|
t.Errorf("SignWithTLSCertificate() - no certificate is given")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSignWithTLSCertificate tests SignWithTLSCertificate with given *tls.Certificate and nil leaf certificate
|
||||||
|
// PLEASE NOTE: Before Go 1.23 Certificate.Leaf was left nil, and the parsed certificate was
|
||||||
|
// discarded. This behavior can be re-enabled by setting "x509keypairleaf=0"
|
||||||
|
// in the GODEBUG environment variable.
|
||||||
|
func TestSignWithTLSCertificate_WithKeyPairLeafNil(t *testing.T) {
|
||||||
|
t.Setenv("GODEBUG", "x509keypairleaf=0")
|
||||||
|
|
||||||
|
keyPairTLS, err := getDummyKeyPairTLS()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to laod dummy crypto material. Cause: %v", err)
|
||||||
|
}
|
||||||
|
m := NewMsg()
|
||||||
|
if err := m.SignWithTLSCertificate(keyPairTLS); err != nil {
|
||||||
|
t.Errorf("failed to set sMime. Cause: %v", err)
|
||||||
|
}
|
||||||
|
if m.sMime.privateKey.ecdsa == nil {
|
||||||
|
t.Errorf("SignWithTLSCertificate() - no private key is given")
|
||||||
|
}
|
||||||
|
if m.sMime.certificate == nil {
|
||||||
|
t.Errorf("SignWithTLSCertificate() - no certificate is given")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSignWithSMime_InvalidPrivateKey tests WithSMimeSinging with given invalid private key
|
||||||
|
func TestSignWithSMime_InvalidPrivateKey(t *testing.T) {
|
||||||
|
m := NewMsg()
|
||||||
|
|
||||||
|
err := m.SignWithSMimeRSA(nil, nil, nil)
|
||||||
|
if !errors.Is(err, ErrInvalidPrivateKey) {
|
||||||
|
t.Errorf("failed to pre-check SignWithSMime method values correctly: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSignWithSMime_InvalidCertificate tests WithSMimeSinging with given invalid certificate
|
||||||
|
func TestSignWithSMime_InvalidCertificate(t *testing.T) {
|
||||||
|
privateKey, _, _, err := getDummyRSACryptoMaterial()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to laod dummy crypto material. Cause: %v", err)
|
||||||
|
}
|
||||||
|
m := NewMsg()
|
||||||
|
|
||||||
|
err = m.SignWithSMimeRSA(privateKey, nil, nil)
|
||||||
|
if !errors.Is(err, ErrInvalidCertificate) {
|
||||||
|
t.Errorf("failed to pre-check SignWithSMime method values correctly: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMsg_createSignaturePart tests the Msg.createSignaturePart method
|
||||||
|
func TestMsg_createSignaturePart(t *testing.T) {
|
||||||
|
privateKey, certificate, intermediateCertificate, err := getDummyRSACryptoMaterial()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to laod dummy crypto material. Cause: %v", err)
|
||||||
|
}
|
||||||
|
m := NewMsg()
|
||||||
|
if err := m.SignWithSMimeRSA(privateKey, certificate, intermediateCertificate); err != nil {
|
||||||
|
t.Errorf("failed to init smime configuration")
|
||||||
|
}
|
||||||
|
body := []byte("This is the body")
|
||||||
|
part, err := m.createSignaturePart(EncodingQP, TypeTextPlain, CharsetUTF7, body)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("createSignaturePart() method failed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if part.GetEncoding() != EncodingB64 {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected encoding: %s, got: %s", EncodingB64, part.GetEncoding())
|
||||||
|
}
|
||||||
|
if part.GetContentType() != typeSMimeSigned {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected content type: %s, got: %s", typeSMimeSigned, part.GetContentType())
|
||||||
|
}
|
||||||
|
if part.GetCharset() != CharsetUTF8 {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected charset: %s, got: %s", CharsetUTF8, part.GetCharset())
|
||||||
|
}
|
||||||
|
if content, err := part.GetContent(); err != nil || len(content) == len(body) {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected content should not be equal: %s, got: %s", body, part.GetEncoding())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestMsg_signMessage tests the Msg.signMessage method
|
||||||
|
func TestMsg_signMessage(t *testing.T) {
|
||||||
|
privateKey, certificate, intermediateCertificate, err := getDummyRSACryptoMaterial()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to laod dummy crypto material. Cause: %v", err)
|
||||||
|
}
|
||||||
|
body := []byte("This is the body")
|
||||||
|
m := NewMsg()
|
||||||
|
m.SetBodyString(TypeTextPlain, string(body))
|
||||||
|
if err := m.SignWithSMimeRSA(privateKey, certificate, intermediateCertificate); err != nil {
|
||||||
|
t.Errorf("failed to init smime configuration")
|
||||||
|
}
|
||||||
|
msg, err := m.signMessage(m)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("createSignaturePart() method failed.")
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := msg.GetParts()
|
||||||
|
if len(parts) != 2 {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected 2 parts, got: %d", len(parts))
|
||||||
|
}
|
||||||
|
|
||||||
|
signedPart := parts[0]
|
||||||
|
if signedPart.GetEncoding() != EncodingQP {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected encoding: %s, got: %s", EncodingB64, signedPart.GetEncoding())
|
||||||
|
}
|
||||||
|
if signedPart.GetContentType() != TypeTextPlain {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected content type: %s, got: %s", typeSMimeSigned, signedPart.GetContentType())
|
||||||
|
}
|
||||||
|
if signedPart.GetCharset() != CharsetUTF8 {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected charset: %s, got: %s", CharsetUTF8, signedPart.GetCharset())
|
||||||
|
}
|
||||||
|
if content, err := signedPart.GetContent(); err != nil || len(content) != len(body) {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected content should be equal: %s, got: %s", body, content)
|
||||||
|
}
|
||||||
|
|
||||||
|
signaturePart := parts[1]
|
||||||
|
if signaturePart.GetEncoding() != EncodingB64 {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected encoding: %s, got: %s", EncodingB64, signaturePart.GetEncoding())
|
||||||
|
}
|
||||||
|
if signaturePart.GetContentType() != typeSMimeSigned {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected content type: %s, got: %s", typeSMimeSigned, signaturePart.GetContentType())
|
||||||
|
}
|
||||||
|
if signaturePart.GetCharset() != CharsetUTF8 {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected charset: %s, got: %s", CharsetUTF8, signaturePart.GetCharset())
|
||||||
|
}
|
||||||
|
if content, err := signaturePart.GetContent(); err != nil || len(content) == len(body) {
|
||||||
|
t.Errorf("createSignaturePart() method failed. Expected content should not be equal: %s, got: %s", body, signaturePart.GetEncoding())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestGetLeafCertificate tests the Msg.getLeafCertificate method
|
||||||
|
func TestGetLeafCertificate(t *testing.T) {
|
||||||
|
keyPairTLS, err := getDummyKeyPairTLS()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to laod dummy crypto material. Cause: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
leafCertificate, err := getLeafCertificate(keyPairTLS)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to get leaf certificate. Cause: %v", err)
|
||||||
|
}
|
||||||
|
if leafCertificate == nil {
|
||||||
|
t.Errorf("failed to get leaf certificate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// uppercaseMiddleware is a middleware type that transforms the subject to uppercase.
|
// uppercaseMiddleware is a middleware type that transforms the subject to uppercase.
|
||||||
type uppercaseMiddleware struct{}
|
type uppercaseMiddleware struct{}
|
||||||
|
|
||||||
|
|
31
msgwriter.go
31
msgwriter.go
|
@ -128,6 +128,10 @@ func (mw *msgWriter) writeMsg(msg *Msg) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if msg.hasSMime() {
|
||||||
|
mw.startMP(MIMESMime, msg.boundary)
|
||||||
|
mw.writeString(DoubleNewLine)
|
||||||
|
}
|
||||||
if msg.hasMixed() {
|
if msg.hasMixed() {
|
||||||
mw.startMP(MIMEMixed, msg.boundary)
|
mw.startMP(MIMEMixed, msg.boundary)
|
||||||
mw.writeString(DoubleNewLine)
|
mw.writeString(DoubleNewLine)
|
||||||
|
@ -174,6 +178,10 @@ func (mw *msgWriter) writeMsg(msg *Msg) {
|
||||||
if msg.hasMixed() {
|
if msg.hasMixed() {
|
||||||
mw.stopMP()
|
mw.stopMP()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if msg.hasSMime() {
|
||||||
|
mw.stopMP()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeGenHeader writes out all generic headers to the msgWriter.
|
// writeGenHeader writes out all generic headers to the msgWriter.
|
||||||
|
@ -314,7 +322,7 @@ func (mw *msgWriter) addFiles(files []*File, isAttachment bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if mw.err == nil {
|
if mw.err == nil {
|
||||||
mw.writeBody(file.Writer, encoding)
|
mw.writeBody(file.Writer, encoding, false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -347,7 +355,12 @@ func (mw *msgWriter) writePart(part *Part, charset Charset) {
|
||||||
if partCharset.String() == "" {
|
if partCharset.String() == "" {
|
||||||
partCharset = charset
|
partCharset = charset
|
||||||
}
|
}
|
||||||
contentType := fmt.Sprintf("%s; charset=%s", part.contentType, partCharset)
|
|
||||||
|
contentType := part.contentType.String()
|
||||||
|
if !part.IsSMimeSigned() {
|
||||||
|
contentType = strings.Join([]string{contentType, "; charset=", partCharset.String()}, "")
|
||||||
|
}
|
||||||
|
|
||||||
contentTransferEnc := part.encoding.String()
|
contentTransferEnc := part.encoding.String()
|
||||||
if mw.depth == 0 {
|
if mw.depth == 0 {
|
||||||
mw.writeHeader(HeaderContentType, contentType)
|
mw.writeHeader(HeaderContentType, contentType)
|
||||||
|
@ -363,7 +376,7 @@ func (mw *msgWriter) writePart(part *Part, charset Charset) {
|
||||||
mimeHeader.Add(string(HeaderContentTransferEnc), contentTransferEnc)
|
mimeHeader.Add(string(HeaderContentTransferEnc), contentTransferEnc)
|
||||||
mw.newPart(mimeHeader)
|
mw.newPart(mimeHeader)
|
||||||
}
|
}
|
||||||
mw.writeBody(part.writeFunc, part.encoding)
|
mw.writeBody(part.writeFunc, part.encoding, part.smime)
|
||||||
}
|
}
|
||||||
|
|
||||||
// writeString writes a string into the msgWriter's io.Writer interface.
|
// writeString writes a string into the msgWriter's io.Writer interface.
|
||||||
|
@ -438,7 +451,8 @@ func (mw *msgWriter) writeHeader(key Header, values ...string) {
|
||||||
// Parameters:
|
// Parameters:
|
||||||
// - writeFunc: A function that writes the body content to the given io.Writer.
|
// - writeFunc: A function that writes the body content to the given io.Writer.
|
||||||
// - encoding: The encoding type to use when writing the content (e.g., base64, quoted-printable).
|
// - encoding: The encoding type to use when writing the content (e.g., base64, quoted-printable).
|
||||||
func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encoding Encoding) {
|
// - singingWithSMime: Whether the msg should be signed with S/MIME or not.
|
||||||
|
func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encoding Encoding, singingWithSMime bool) {
|
||||||
var writer io.Writer
|
var writer io.Writer
|
||||||
var encodedWriter io.WriteCloser
|
var encodedWriter io.WriteCloser
|
||||||
var n int64
|
var n int64
|
||||||
|
@ -453,12 +467,11 @@ func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encodin
|
||||||
lineBreaker := Base64LineBreaker{}
|
lineBreaker := Base64LineBreaker{}
|
||||||
lineBreaker.out = &writeBuffer
|
lineBreaker.out = &writeBuffer
|
||||||
|
|
||||||
switch encoding {
|
if encoding == EncodingQP {
|
||||||
case EncodingQP:
|
|
||||||
encodedWriter = quotedprintable.NewWriter(&writeBuffer)
|
encodedWriter = quotedprintable.NewWriter(&writeBuffer)
|
||||||
case EncodingB64:
|
} else if encoding == EncodingB64 && !singingWithSMime {
|
||||||
encodedWriter = base64.NewEncoder(base64.StdEncoding, &lineBreaker)
|
encodedWriter = base64.NewEncoder(base64.StdEncoding, &lineBreaker)
|
||||||
case NoEncoding:
|
} else if encoding == NoEncoding || singingWithSMime {
|
||||||
_, err = writeFunc(&writeBuffer)
|
_, err = writeFunc(&writeBuffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mw.err = fmt.Errorf("bodyWriter function: %w", err)
|
mw.err = fmt.Errorf("bodyWriter function: %w", err)
|
||||||
|
@ -471,7 +484,7 @@ func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encodin
|
||||||
mw.bytesWritten += n
|
mw.bytesWritten += n
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
default:
|
} else {
|
||||||
encodedWriter = quotedprintable.NewWriter(writer)
|
encodedWriter = quotedprintable.NewWriter(writer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -620,7 +620,7 @@ func TestMsgWriter_writeBody(t *testing.T) {
|
||||||
buffer := bytes.NewBuffer(nil)
|
buffer := bytes.NewBuffer(nil)
|
||||||
msgwriter.writer = buffer
|
msgwriter.writer = buffer
|
||||||
message := testMessage(t)
|
message := testMessage(t)
|
||||||
msgwriter.writeBody(message.parts[0].writeFunc, NoEncoding)
|
msgwriter.writeBody(message.parts[0].writeFunc, NoEncoding, false)
|
||||||
if msgwriter.err != nil {
|
if msgwriter.err != nil {
|
||||||
t.Errorf("writeBody failed to write: %s", msgwriter.err)
|
t.Errorf("writeBody failed to write: %s", msgwriter.err)
|
||||||
}
|
}
|
||||||
|
@ -628,7 +628,7 @@ func TestMsgWriter_writeBody(t *testing.T) {
|
||||||
t.Run("writeBody on NoEncoding fails on write", func(t *testing.T) {
|
t.Run("writeBody on NoEncoding fails on write", func(t *testing.T) {
|
||||||
msgwriter.writer = failReadWriteSeekCloser{}
|
msgwriter.writer = failReadWriteSeekCloser{}
|
||||||
message := testMessage(t)
|
message := testMessage(t)
|
||||||
msgwriter.writeBody(message.parts[0].writeFunc, NoEncoding)
|
msgwriter.writeBody(message.parts[0].writeFunc, NoEncoding, false)
|
||||||
if msgwriter.err == nil {
|
if msgwriter.err == nil {
|
||||||
t.Errorf("writeBody succeeded, expected error")
|
t.Errorf("writeBody succeeded, expected error")
|
||||||
}
|
}
|
||||||
|
@ -642,7 +642,7 @@ func TestMsgWriter_writeBody(t *testing.T) {
|
||||||
writeFunc := func(io.Writer) (int64, error) {
|
writeFunc := func(io.Writer) (int64, error) {
|
||||||
return 0, errors.New("intentional write failure")
|
return 0, errors.New("intentional write failure")
|
||||||
}
|
}
|
||||||
msgwriter.writeBody(writeFunc, NoEncoding)
|
msgwriter.writeBody(writeFunc, NoEncoding, false)
|
||||||
if msgwriter.err == nil {
|
if msgwriter.err == nil {
|
||||||
t.Errorf("writeBody succeeded, expected error")
|
t.Errorf("writeBody succeeded, expected error")
|
||||||
}
|
}
|
||||||
|
@ -653,7 +653,7 @@ func TestMsgWriter_writeBody(t *testing.T) {
|
||||||
t.Run("writeBody Quoted-Printable fails on write", func(t *testing.T) {
|
t.Run("writeBody Quoted-Printable fails on write", func(t *testing.T) {
|
||||||
msgwriter.writer = failReadWriteSeekCloser{}
|
msgwriter.writer = failReadWriteSeekCloser{}
|
||||||
message := testMessage(t)
|
message := testMessage(t)
|
||||||
msgwriter.writeBody(message.parts[0].writeFunc, EncodingQP)
|
msgwriter.writeBody(message.parts[0].writeFunc, EncodingQP, false)
|
||||||
if msgwriter.err == nil {
|
if msgwriter.err == nil {
|
||||||
t.Errorf("writeBody succeeded, expected error")
|
t.Errorf("writeBody succeeded, expected error")
|
||||||
}
|
}
|
||||||
|
@ -667,7 +667,7 @@ func TestMsgWriter_writeBody(t *testing.T) {
|
||||||
writeFunc := func(io.Writer) (int64, error) {
|
writeFunc := func(io.Writer) (int64, error) {
|
||||||
return 0, errors.New("intentional write failure")
|
return 0, errors.New("intentional write failure")
|
||||||
}
|
}
|
||||||
msgwriter.writeBody(writeFunc, EncodingQP)
|
msgwriter.writeBody(writeFunc, EncodingQP, false)
|
||||||
if msgwriter.err == nil {
|
if msgwriter.err == nil {
|
||||||
t.Errorf("writeBody succeeded, expected error")
|
t.Errorf("writeBody succeeded, expected error")
|
||||||
}
|
}
|
||||||
|
@ -676,3 +676,42 @@ func TestMsgWriter_writeBody(t *testing.T) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestMsgWriter_writeMsg_SMime tests the writeMsg method of the msgWriter with S/MIME types set
|
||||||
|
func TestMsgWriter_writeMsg_SMime(t *testing.T) {
|
||||||
|
privateKey, certificate, intermediateCertificate, err := getDummyRSACryptoMaterial()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to laod dummy crypto material. Cause: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := NewMsg()
|
||||||
|
if err := m.SignWithSMimeRSA(privateKey, certificate, intermediateCertificate); err != nil {
|
||||||
|
t.Errorf("failed to init smime configuration")
|
||||||
|
}
|
||||||
|
_ = m.From(`"Toni Tester" <test@example.com>`)
|
||||||
|
_ = m.To(`"Toni Receiver" <receiver@example.com>`)
|
||||||
|
m.Subject("This is a subject")
|
||||||
|
m.SetBodyString(TypeTextPlain, "This is the body")
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
mw := &msgWriter{writer: &buf, charset: CharsetUTF8, encoder: mime.QEncoding}
|
||||||
|
mw.writeMsg(m)
|
||||||
|
ms := buf.String()
|
||||||
|
|
||||||
|
if !strings.Contains(ms, "MIME-Version: 1.0") {
|
||||||
|
t.Errorf("writeMsg failed. Unable to find MIME-Version")
|
||||||
|
}
|
||||||
|
if !strings.Contains(ms, "Subject: This is a subject") {
|
||||||
|
t.Errorf("writeMsg failed. Unable to find subject")
|
||||||
|
}
|
||||||
|
if !strings.Contains(ms, "From: \"Toni Tester\" <test@example.com>") {
|
||||||
|
t.Errorf("writeMsg failed. Unable to find transmitter")
|
||||||
|
}
|
||||||
|
if !strings.Contains(ms, "To: \"Toni Receiver\" <receiver@example.com>") {
|
||||||
|
t.Errorf("writeMsg failed. Unable to find receiver")
|
||||||
|
}
|
||||||
|
|
||||||
|
boundary := ms[strings.LastIndex(ms, "--")-60 : strings.LastIndex(ms, "--")]
|
||||||
|
if !strings.Contains(ms, fmt.Sprintf("Content-Type: multipart/signed; protocol=\"application/pkcs7-signature\"; micalg=sha-256;\r\n boundary=%s", boundary)) {
|
||||||
|
t.Errorf("writeMsg failed. Unable to find Content-Type")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
18
part.go
18
part.go
|
@ -24,6 +24,7 @@ type Part struct {
|
||||||
encoding Encoding
|
encoding Encoding
|
||||||
isDeleted bool
|
isDeleted bool
|
||||||
writeFunc func(io.Writer) (int64, error)
|
writeFunc func(io.Writer) (int64, error)
|
||||||
|
smime bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetContent executes the WriteFunc of the Part and returns the content as a byte slice.
|
// GetContent executes the WriteFunc of the Part and returns the content as a byte slice.
|
||||||
|
@ -94,6 +95,11 @@ func (p *Part) GetDescription() string {
|
||||||
return p.description
|
return p.description
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsSMimeSigned returns true if the Part should be singed with S/MIME
|
||||||
|
func (p *Part) IsSMimeSigned() bool {
|
||||||
|
return p.smime
|
||||||
|
}
|
||||||
|
|
||||||
// SetContent overrides the content of the Part with the given string.
|
// SetContent overrides the content of the Part with the given string.
|
||||||
//
|
//
|
||||||
// This function sets the content of the Part by creating a new writeFunc that writes the
|
// This function sets the content of the Part by creating a new writeFunc that writes the
|
||||||
|
@ -146,6 +152,11 @@ func (p *Part) SetDescription(description string) {
|
||||||
p.description = description
|
p.description = description
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetIsSMimeSigned sets the flag for signing the Part with S/MIME
|
||||||
|
func (p *Part) SetIsSMimeSigned(smime bool) {
|
||||||
|
p.smime = smime
|
||||||
|
}
|
||||||
|
|
||||||
// SetWriteFunc overrides the WriteFunc of the Part.
|
// SetWriteFunc overrides the WriteFunc of the Part.
|
||||||
//
|
//
|
||||||
// This function sets a new WriteFunc for the Part, replacing the existing one. The WriteFunc
|
// This function sets a new WriteFunc for the Part, replacing the existing one. The WriteFunc
|
||||||
|
@ -213,3 +224,10 @@ func WithPartContentDescription(description string) PartOption {
|
||||||
p.description = description
|
p.description = description
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithSMimeSinging overrides the flag for signing the Part with S/MIME
|
||||||
|
func WithSMimeSinging() PartOption {
|
||||||
|
return func(p *Part) {
|
||||||
|
p.smime = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
45
part_test.go
45
part_test.go
|
@ -102,6 +102,24 @@ func TestPart_WithPartContentDescription(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestPart_WithSMimeSinging tests the WithSMimeSinging method
|
||||||
|
func TestPart_WithSMimeSinging(t *testing.T) {
|
||||||
|
m := NewMsg()
|
||||||
|
part := m.newPart(TypeTextPlain, WithSMimeSinging())
|
||||||
|
if part == nil {
|
||||||
|
t.Errorf("newPart() WithSMimeSinging() failed: no part returned")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if part.smime != true {
|
||||||
|
t.Errorf("newPart() WithSMimeSinging() failed: expected: %v, got: %v", true, part.smime)
|
||||||
|
}
|
||||||
|
part.smime = true
|
||||||
|
part.SetIsSMimeSigned(false)
|
||||||
|
if part.smime != false {
|
||||||
|
t.Errorf("newPart() SetIsSMimeSigned() failed: expected: %v, got: %v", false, part.smime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestPartContentType tests Part.SetContentType
|
// TestPartContentType tests Part.SetContentType
|
||||||
func TestPart_SetContentType(t *testing.T) {
|
func TestPart_SetContentType(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
@ -245,6 +263,33 @@ func TestPart_GetContentBroken(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestPart_IsSMimeSigned tests Part.IsSMimeSigned
|
||||||
|
func TestPart_IsSMimeSigned(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{"smime:", true},
|
||||||
|
{"smime:", false},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
m := NewMsg()
|
||||||
|
m.SetBodyString(TypeTextPlain, "This is a body!")
|
||||||
|
pl, err := getPartList(m)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed: %s", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pl[0].SetIsSMimeSigned(tt.want)
|
||||||
|
smime := pl[0].IsSMimeSigned()
|
||||||
|
if smime != tt.want {
|
||||||
|
t.Errorf("SetContentType failed. Got: %v, expected: %v", smime, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestPart_SetWriteFunc tests Part.SetWriteFunc
|
// TestPart_SetWriteFunc tests Part.SetWriteFunc
|
||||||
func TestPart_SetWriteFunc(t *testing.T) {
|
func TestPart_SetWriteFunc(t *testing.T) {
|
||||||
c := "This is a test with ümläutß"
|
c := "This is a test with ümläutß"
|
||||||
|
|
388
pkcs7.go
Normal file
388
pkcs7.go
Normal file
|
@ -0,0 +1,388 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/asn1"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
_ "crypto/sha1" // for crypto.SHA1
|
||||||
|
)
|
||||||
|
|
||||||
|
// PKCS7 Represents a PKCS7 structure
|
||||||
|
type PKCS7 struct {
|
||||||
|
Content []byte
|
||||||
|
Certificates []*x509.Certificate
|
||||||
|
CRLs []x509.RevocationList
|
||||||
|
Signers []signerInfo
|
||||||
|
raw interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type contentInfo struct {
|
||||||
|
ContentType asn1.ObjectIdentifier
|
||||||
|
Content asn1.RawValue `asn1:"explicit,optional,tag:0"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
oidData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 1}
|
||||||
|
oidSignedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 2}
|
||||||
|
oidAttributeContentType = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 3}
|
||||||
|
oidAttributeMessageDigest = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 4}
|
||||||
|
oidAttributeSigningTime = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 5}
|
||||||
|
)
|
||||||
|
|
||||||
|
type signedData struct {
|
||||||
|
Version int `asn1:"default:1"`
|
||||||
|
DigestAlgorithmIdentifiers []pkix.AlgorithmIdentifier `asn1:"set"`
|
||||||
|
ContentInfo contentInfo
|
||||||
|
Certificates rawCertificates `asn1:"optional,tag:0"`
|
||||||
|
CRLs []x509.RevocationList `asn1:"optional,tag:1"`
|
||||||
|
SignerInfos []signerInfo `asn1:"set"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type rawCertificates struct {
|
||||||
|
Raw asn1.RawContent
|
||||||
|
}
|
||||||
|
|
||||||
|
type attribute struct {
|
||||||
|
Type asn1.ObjectIdentifier
|
||||||
|
Value asn1.RawValue `asn1:"set"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type issuerAndSerial struct {
|
||||||
|
IssuerName asn1.RawValue
|
||||||
|
SerialNumber *big.Int
|
||||||
|
}
|
||||||
|
|
||||||
|
// MessageDigestMismatchError is returned when the signer data digest does not
|
||||||
|
// match the computed digest for the contained content
|
||||||
|
type MessageDigestMismatchError struct {
|
||||||
|
ExpectedDigest []byte
|
||||||
|
ActualDigest []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err *MessageDigestMismatchError) Error() string {
|
||||||
|
return fmt.Sprintf("pkcs7: Message digest mismatch\n\tExpected: %X\n\tActual : %X", err.ExpectedDigest, err.ActualDigest)
|
||||||
|
}
|
||||||
|
|
||||||
|
type signerInfo struct {
|
||||||
|
Version int `asn1:"default:1"`
|
||||||
|
IssuerAndSerialNumber issuerAndSerial
|
||||||
|
DigestAlgorithm pkix.AlgorithmIdentifier
|
||||||
|
AuthenticatedAttributes []attribute `asn1:"optional,tag:0"`
|
||||||
|
DigestEncryptionAlgorithm pkix.AlgorithmIdentifier
|
||||||
|
EncryptedDigest []byte
|
||||||
|
UnauthenticatedAttributes []attribute `asn1:"optional,tag:1"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (raw rawCertificates) Parse() ([]*x509.Certificate, error) {
|
||||||
|
if len(raw.Raw) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var val asn1.RawValue
|
||||||
|
if _, err := asn1.Unmarshal(raw.Raw, &val); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return x509.ParseCertificates(val.Bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func marshalAttributes(attrs []attribute) ([]byte, error) {
|
||||||
|
encodedAttributes, err := asn1.Marshal(struct {
|
||||||
|
A []attribute `asn1:"set"`
|
||||||
|
}{A: attrs})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the leading sequence octets
|
||||||
|
var raw asn1.RawValue
|
||||||
|
if _, err := asn1.Unmarshal(encodedAttributes, &raw); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return raw.Bytes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
oidDigestAlgorithmSHA1 = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 26}
|
||||||
|
oidSignatureSHA1WithRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 5}
|
||||||
|
)
|
||||||
|
|
||||||
|
func getCertFromCertsByIssuerAndSerial(certs []*x509.Certificate, ias issuerAndSerial) *x509.Certificate {
|
||||||
|
for _, cert := range certs {
|
||||||
|
if isCertMatchForIssuerAndSerial(cert, ias) {
|
||||||
|
return cert
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOnlySigner returns an x509.Certificate for the first signer of the signed
|
||||||
|
// data payload. If there are more or less than one signer, nil is returned
|
||||||
|
func (p7 *PKCS7) GetOnlySigner() *x509.Certificate {
|
||||||
|
if len(p7.Signers) != 1 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
signer := p7.Signers[0]
|
||||||
|
return getCertFromCertsByIssuerAndSerial(p7.Certificates, signer.IssuerAndSerialNumber)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrUnsupportedAlgorithm tells you when our quick dev assumptions have failed
|
||||||
|
var ErrUnsupportedAlgorithm = errors.New("pkcs7: cannot decrypt data: only RSA, DES, DES-EDE3, AES-256-CBC and AES-128-GCM supported")
|
||||||
|
|
||||||
|
func isCertMatchForIssuerAndSerial(cert *x509.Certificate, ias issuerAndSerial) bool {
|
||||||
|
return cert.SerialNumber.Cmp(ias.SerialNumber) == 0 && bytes.Equal(cert.RawIssuer, ias.IssuerName.FullBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshalAttribute(attrs []attribute, attributeType asn1.ObjectIdentifier, out interface{}) error {
|
||||||
|
for _, attr := range attrs {
|
||||||
|
if attr.Type.Equal(attributeType) {
|
||||||
|
_, err := asn1.Unmarshal(attr.Value.Bytes, out)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return errors.New("pkcs7: attribute type not in attributes")
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalSignedAttribute decodes a single attribute from the signer info
|
||||||
|
func (p7 *PKCS7) UnmarshalSignedAttribute(attributeType asn1.ObjectIdentifier, out interface{}) error {
|
||||||
|
sd, ok := p7.raw.(signedData)
|
||||||
|
if !ok {
|
||||||
|
return errors.New("pkcs7: payload is not signedData content")
|
||||||
|
}
|
||||||
|
if len(sd.SignerInfos) < 1 {
|
||||||
|
return errors.New("pkcs7: payload has no signers")
|
||||||
|
}
|
||||||
|
attributes := sd.SignerInfos[0].AuthenticatedAttributes
|
||||||
|
return unmarshalAttribute(attributes, attributeType, out)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignedData is an opaque data structure for creating signed data payloads
|
||||||
|
type SignedData struct {
|
||||||
|
sd signedData
|
||||||
|
certs []*x509.Certificate
|
||||||
|
messageDigest []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attribute represents a key value pair attribute. Value must be marshalable byte
|
||||||
|
// `encoding/asn1`
|
||||||
|
type Attribute struct {
|
||||||
|
Type asn1.ObjectIdentifier
|
||||||
|
Value interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignerInfoConfig are optional values to include when adding a signer
|
||||||
|
type SignerInfoConfig struct {
|
||||||
|
ExtraSignedAttributes []Attribute
|
||||||
|
}
|
||||||
|
|
||||||
|
// newSignedData initializes a SignedData with content
|
||||||
|
func newSignedData(data []byte) (*SignedData, error) {
|
||||||
|
content, err := asn1.Marshal(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ci := contentInfo{
|
||||||
|
ContentType: oidData,
|
||||||
|
Content: asn1.RawValue{Class: 2, Tag: 0, Bytes: content, IsCompound: true},
|
||||||
|
}
|
||||||
|
digAlg := pkix.AlgorithmIdentifier{
|
||||||
|
Algorithm: oidDigestAlgorithmSHA1,
|
||||||
|
}
|
||||||
|
h := crypto.SHA1.New()
|
||||||
|
h.Write(data)
|
||||||
|
md := h.Sum(nil)
|
||||||
|
sd := signedData{
|
||||||
|
ContentInfo: ci,
|
||||||
|
Version: 1,
|
||||||
|
DigestAlgorithmIdentifiers: []pkix.AlgorithmIdentifier{digAlg},
|
||||||
|
}
|
||||||
|
return &SignedData{sd: sd, messageDigest: md}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type attributes struct {
|
||||||
|
types []asn1.ObjectIdentifier
|
||||||
|
values []interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add adds the attribute, maintaining insertion order
|
||||||
|
func (attrs *attributes) Add(attrType asn1.ObjectIdentifier, value interface{}) {
|
||||||
|
attrs.types = append(attrs.types, attrType)
|
||||||
|
attrs.values = append(attrs.values, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
type sortableAttribute struct {
|
||||||
|
SortKey []byte
|
||||||
|
Attribute attribute
|
||||||
|
}
|
||||||
|
|
||||||
|
type attributeSet []sortableAttribute
|
||||||
|
|
||||||
|
func (sa attributeSet) Len() int {
|
||||||
|
return len(sa)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sa attributeSet) Less(i, j int) bool {
|
||||||
|
return bytes.Compare(sa[i].SortKey, sa[j].SortKey) < 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sa attributeSet) Swap(i, j int) {
|
||||||
|
sa[i], sa[j] = sa[j], sa[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sa attributeSet) attributes() []attribute {
|
||||||
|
attrs := make([]attribute, len(sa))
|
||||||
|
for i, attr := range sa {
|
||||||
|
attrs[i] = attr.Attribute
|
||||||
|
}
|
||||||
|
return attrs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (attrs *attributes) forMarshaling() ([]attribute, error) {
|
||||||
|
sortables := make(attributeSet, len(attrs.types))
|
||||||
|
for i := range sortables {
|
||||||
|
attrType := attrs.types[i]
|
||||||
|
attrValue := attrs.values[i]
|
||||||
|
asn1Value, err := asn1.Marshal(attrValue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
attr := attribute{
|
||||||
|
Type: attrType,
|
||||||
|
Value: asn1.RawValue{Tag: 17, IsCompound: true, Bytes: asn1Value}, // 17 == SET tag
|
||||||
|
}
|
||||||
|
encoded, err := asn1.Marshal(attr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sortables[i] = sortableAttribute{
|
||||||
|
SortKey: encoded,
|
||||||
|
Attribute: attr,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Sort(sortables)
|
||||||
|
return sortables.attributes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addSigner signs attributes about the content and adds certificate to payload
|
||||||
|
func (sd *SignedData) addSigner(cert *x509.Certificate, pkey crypto.PrivateKey, config SignerInfoConfig) error {
|
||||||
|
attrs := &attributes{}
|
||||||
|
attrs.Add(oidAttributeContentType, sd.sd.ContentInfo.ContentType)
|
||||||
|
attrs.Add(oidAttributeMessageDigest, sd.messageDigest)
|
||||||
|
attrs.Add(oidAttributeSigningTime, time.Now())
|
||||||
|
for _, attr := range config.ExtraSignedAttributes {
|
||||||
|
attrs.Add(attr.Type, attr.Value)
|
||||||
|
}
|
||||||
|
finalAttrs, err := attrs.forMarshaling()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
signature, err := signAttributes(finalAttrs, pkey, crypto.SHA1)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ias, err := cert2issuerAndSerial(cert)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
signer := signerInfo{
|
||||||
|
AuthenticatedAttributes: finalAttrs,
|
||||||
|
DigestAlgorithm: pkix.AlgorithmIdentifier{Algorithm: oidDigestAlgorithmSHA1},
|
||||||
|
DigestEncryptionAlgorithm: pkix.AlgorithmIdentifier{Algorithm: oidSignatureSHA1WithRSA},
|
||||||
|
IssuerAndSerialNumber: ias,
|
||||||
|
EncryptedDigest: signature,
|
||||||
|
Version: 1,
|
||||||
|
}
|
||||||
|
// create signature of signed attributes
|
||||||
|
sd.certs = append(sd.certs, cert)
|
||||||
|
sd.sd.SignerInfos = append(sd.sd.SignerInfos, signer)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addCertificate adds the certificate to the payload. Useful for parent certificates
|
||||||
|
func (sd *SignedData) addCertificate(cert *x509.Certificate) {
|
||||||
|
sd.certs = append(sd.certs, cert)
|
||||||
|
}
|
||||||
|
|
||||||
|
// detach removes content from the signed data struct to make it a detached signature.
|
||||||
|
// This must be called right before Finish()
|
||||||
|
func (sd *SignedData) detach() {
|
||||||
|
sd.sd.ContentInfo = contentInfo{ContentType: oidData}
|
||||||
|
}
|
||||||
|
|
||||||
|
// finish marshals the content and its signers
|
||||||
|
func (sd *SignedData) finish() ([]byte, error) {
|
||||||
|
sd.sd.Certificates = marshalCertificates(sd.certs)
|
||||||
|
inner, err := asn1.Marshal(sd.sd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
outer := contentInfo{
|
||||||
|
ContentType: oidSignedData,
|
||||||
|
Content: asn1.RawValue{Class: 2, Tag: 0, Bytes: inner, IsCompound: true},
|
||||||
|
}
|
||||||
|
return asn1.Marshal(outer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func cert2issuerAndSerial(cert *x509.Certificate) (issuerAndSerial, error) {
|
||||||
|
var ias issuerAndSerial
|
||||||
|
// The issuer RDNSequence has to match exactly the sequence in the certificate
|
||||||
|
// We cannot use cert.Issuer.ToRDNSequence() here since it mangles the sequence
|
||||||
|
ias.IssuerName = asn1.RawValue{FullBytes: cert.RawIssuer}
|
||||||
|
ias.SerialNumber = cert.SerialNumber
|
||||||
|
|
||||||
|
return ias, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// signs the DER encoded form of the attributes with the private key
|
||||||
|
func signAttributes(attrs []attribute, pkey crypto.PrivateKey, hash crypto.Hash) ([]byte, error) {
|
||||||
|
attrBytes, err := marshalAttributes(attrs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
h := hash.New()
|
||||||
|
h.Write(attrBytes)
|
||||||
|
hashed := h.Sum(nil)
|
||||||
|
switch priv := pkey.(type) {
|
||||||
|
case *rsa.PrivateKey:
|
||||||
|
return rsa.SignPKCS1v15(rand.Reader, priv, crypto.SHA1, hashed)
|
||||||
|
}
|
||||||
|
return nil, ErrUnsupportedAlgorithm
|
||||||
|
}
|
||||||
|
|
||||||
|
// concats and wraps the certificates in the RawValue structure
|
||||||
|
func marshalCertificates(certs []*x509.Certificate) rawCertificates {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
for _, cert := range certs {
|
||||||
|
buf.Write(cert.Raw)
|
||||||
|
}
|
||||||
|
rawCerts, _ := marshalCertificateBytes(buf.Bytes())
|
||||||
|
return rawCerts
|
||||||
|
}
|
||||||
|
|
||||||
|
// Even though, the tag & length are stripped out during marshalling the
|
||||||
|
// RawContent, we have to encode it into the RawContent. If its missing,
|
||||||
|
// then `asn1.Marshal()` will strip out the certificate wrapper instead.
|
||||||
|
func marshalCertificateBytes(certs []byte) (rawCertificates, error) {
|
||||||
|
val := asn1.RawValue{Bytes: certs, Class: 2, Tag: 0, IsCompound: true}
|
||||||
|
b, err := asn1.Marshal(val)
|
||||||
|
if err != nil {
|
||||||
|
return rawCertificates{}, err
|
||||||
|
}
|
||||||
|
return rawCertificates{Raw: b}, nil
|
||||||
|
}
|
123
pkcs7_test.go
Normal file
123
pkcs7_test.go
Normal file
|
@ -0,0 +1,123 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestSign_E2E tests S/MIME singing as e2e
|
||||||
|
func TestSign_E2E(t *testing.T) {
|
||||||
|
cert, err := createTestCertificate()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
content := []byte("Hello World")
|
||||||
|
for _, testDetach := range []bool{false, true} {
|
||||||
|
toBeSigned, err := newSignedData(content)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Cannot initialize signed data: %s", err)
|
||||||
|
}
|
||||||
|
if err := toBeSigned.addSigner(cert.Certificate, cert.PrivateKey, SignerInfoConfig{}); err != nil {
|
||||||
|
t.Fatalf("Cannot add signer: %s", err)
|
||||||
|
}
|
||||||
|
if testDetach {
|
||||||
|
t.Log("Testing detached signature")
|
||||||
|
toBeSigned.detach()
|
||||||
|
} else {
|
||||||
|
t.Log("Testing attached signature")
|
||||||
|
}
|
||||||
|
signed, err := toBeSigned.finish()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Cannot finish signing data: %s", err)
|
||||||
|
}
|
||||||
|
if err := pem.Encode(os.Stdout, &pem.Block{Type: "PKCS7", Bytes: signed}); err != nil {
|
||||||
|
t.Fatalf("Cannot write signed data: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type certKeyPair struct {
|
||||||
|
Certificate *x509.Certificate
|
||||||
|
PrivateKey *rsa.PrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTestCertificate() (*certKeyPair, error) {
|
||||||
|
signer, err := createTestCertificateByIssuer("Eddard Stark", nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fmt.Println("Created root cert")
|
||||||
|
if err := pem.Encode(os.Stdout, &pem.Block{Type: "CERTIFICATE", Bytes: signer.Certificate.Raw}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pair, err := createTestCertificateByIssuer("Jon Snow", signer)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fmt.Println("Created signer cert")
|
||||||
|
if err := pem.Encode(os.Stdout, &pem.Block{Type: "CERTIFICATE", Bytes: pair.Certificate.Raw}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pair, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createTestCertificateByIssuer(name string, issuer *certKeyPair) (*certKeyPair, error) {
|
||||||
|
priv, err := rsa.GenerateKey(rand.Reader, 1024)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 32)
|
||||||
|
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
template := x509.Certificate{
|
||||||
|
SerialNumber: serialNumber,
|
||||||
|
SignatureAlgorithm: x509.SHA256WithRSA,
|
||||||
|
Subject: pkix.Name{
|
||||||
|
CommonName: name,
|
||||||
|
Organization: []string{"Acme Co"},
|
||||||
|
},
|
||||||
|
NotBefore: time.Now(),
|
||||||
|
NotAfter: time.Now().AddDate(1, 0, 0),
|
||||||
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageEmailProtection},
|
||||||
|
}
|
||||||
|
var issuerCert *x509.Certificate
|
||||||
|
var issuerKey crypto.PrivateKey
|
||||||
|
if issuer != nil {
|
||||||
|
issuerCert = issuer.Certificate
|
||||||
|
issuerKey = issuer.PrivateKey
|
||||||
|
} else {
|
||||||
|
template.IsCA = true
|
||||||
|
template.KeyUsage |= x509.KeyUsageCertSign
|
||||||
|
issuerCert = &template
|
||||||
|
issuerKey = priv
|
||||||
|
}
|
||||||
|
cert, err := x509.CreateCertificate(rand.Reader, &template, issuerCert, priv.Public(), issuerKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
leaf, err := x509.ParseCertificate(cert)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &certKeyPair{
|
||||||
|
Certificate: leaf,
|
||||||
|
PrivateKey: priv,
|
||||||
|
}, nil
|
||||||
|
}
|
217
smime.go
Normal file
217
smime.go
Normal file
|
@ -0,0 +1,217 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"mime/quotedprintable"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrInvalidPrivateKey should be used if private key is invalid
|
||||||
|
ErrInvalidPrivateKey = errors.New("invalid private key")
|
||||||
|
|
||||||
|
// ErrInvalidCertificate should be used if the certificate is invalid
|
||||||
|
ErrInvalidCertificate = errors.New("invalid certificate")
|
||||||
|
)
|
||||||
|
|
||||||
|
// privateKeyHolder is the representation of a private key
|
||||||
|
type privateKeyHolder struct {
|
||||||
|
ecdsa *ecdsa.PrivateKey
|
||||||
|
rsa *rsa.PrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
// get returns the private key of the privateKeyHolder
|
||||||
|
func (p privateKeyHolder) get() crypto.PrivateKey {
|
||||||
|
if p.ecdsa != nil {
|
||||||
|
return p.ecdsa
|
||||||
|
}
|
||||||
|
return p.rsa
|
||||||
|
}
|
||||||
|
|
||||||
|
// SMime is used to sign messages with S/MIME
|
||||||
|
type SMime struct {
|
||||||
|
privateKey privateKeyHolder
|
||||||
|
certificate *x509.Certificate
|
||||||
|
intermediateCertificate *x509.Certificate
|
||||||
|
}
|
||||||
|
|
||||||
|
// newSMimeWithRSA construct a new instance of SMime with provided parameters
|
||||||
|
// privateKey as *rsa.PrivateKey
|
||||||
|
// certificate as *x509.Certificate
|
||||||
|
// intermediateCertificate (optional) as *x509.Certificate
|
||||||
|
func newSMimeWithRSA(privateKey *rsa.PrivateKey, certificate *x509.Certificate, intermediateCertificate *x509.Certificate) (*SMime, error) {
|
||||||
|
if privateKey == nil {
|
||||||
|
return nil, ErrInvalidPrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if certificate == nil {
|
||||||
|
return nil, ErrInvalidCertificate
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SMime{
|
||||||
|
privateKey: privateKeyHolder{rsa: privateKey},
|
||||||
|
certificate: certificate,
|
||||||
|
intermediateCertificate: intermediateCertificate,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// newSMimeWithECDSA construct a new instance of SMime with provided parameters
|
||||||
|
// privateKey as *ecdsa.PrivateKey
|
||||||
|
// certificate as *x509.Certificate
|
||||||
|
// intermediateCertificate (optional) as *x509.Certificate
|
||||||
|
func newSMimeWithECDSA(privateKey *ecdsa.PrivateKey, certificate *x509.Certificate, intermediateCertificate *x509.Certificate) (*SMime, error) {
|
||||||
|
if privateKey == nil {
|
||||||
|
return nil, ErrInvalidPrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if certificate == nil {
|
||||||
|
return nil, ErrInvalidCertificate
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SMime{
|
||||||
|
privateKey: privateKeyHolder{ecdsa: privateKey},
|
||||||
|
certificate: certificate,
|
||||||
|
intermediateCertificate: intermediateCertificate,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// signMessage signs the message with S/MIME
|
||||||
|
func (sm *SMime) signMessage(message string) (*string, error) {
|
||||||
|
lines := parseLines([]byte(message))
|
||||||
|
toBeSigned := lines.bytesFromLines([]byte("\r\n"))
|
||||||
|
|
||||||
|
signedData, err := newSignedData(toBeSigned)
|
||||||
|
if err != nil || signedData == nil {
|
||||||
|
return nil, fmt.Errorf("could not initialize signed data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = signedData.addSigner(sm.certificate, sm.privateKey.get(), SignerInfoConfig{}); err != nil {
|
||||||
|
return nil, fmt.Errorf("could not add signer message: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sm.intermediateCertificate != nil {
|
||||||
|
signedData.addCertificate(sm.intermediateCertificate)
|
||||||
|
}
|
||||||
|
|
||||||
|
signedData.detach()
|
||||||
|
|
||||||
|
signatureDER, err := signedData.finish()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not finish signing: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pemMsg, err := encodeToPEM(signatureDER)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not encode to PEM: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pemMsg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// createMessage prepares the message that will be used for the sign method later
|
||||||
|
func (sm *SMime) prepareMessage(encoding Encoding, contentType ContentType, charset Charset, body []byte) (*string, error) {
|
||||||
|
encodedMessage, err := sm.encodeMessage(encoding, string(body))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
preparedMessage := fmt.Sprintf("Content-Transfer-Encoding: %v\r\nContent-Type: %v; charset=%v\r\n\r\n%v", encoding, contentType, charset, *encodedMessage)
|
||||||
|
return &preparedMessage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeMessage encodes the message with the given encoding
|
||||||
|
func (sm *SMime) encodeMessage(encoding Encoding, message string) (*string, error) {
|
||||||
|
if encoding != EncodingQP {
|
||||||
|
return &message, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer := bytes.Buffer{}
|
||||||
|
writer := quotedprintable.NewWriter(&buffer)
|
||||||
|
if _, err := writer.Write([]byte(message)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := writer.Close(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
encodedMessage := buffer.String()
|
||||||
|
|
||||||
|
return &encodedMessage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// encodeToPEM uses the method pem.Encode from the standard library but cuts the typical PEM preamble
|
||||||
|
func encodeToPEM(msg []byte) (*string, error) {
|
||||||
|
block := &pem.Block{Bytes: msg}
|
||||||
|
|
||||||
|
var arrayBuffer bytes.Buffer
|
||||||
|
if err := pem.Encode(&arrayBuffer, block); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r := arrayBuffer.String()
|
||||||
|
r = strings.TrimPrefix(r, "-----BEGIN -----")
|
||||||
|
r = strings.Trim(r, "\n")
|
||||||
|
r = strings.TrimSuffix(r, "-----END -----")
|
||||||
|
r = strings.Trim(r, "\n")
|
||||||
|
|
||||||
|
return &r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// line is the representation of one line of the message that will be used for signing purposes
|
||||||
|
type line struct {
|
||||||
|
line []byte
|
||||||
|
endOfLine []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// lines is the representation of a message that will be used for signing purposes
|
||||||
|
type lines []line
|
||||||
|
|
||||||
|
// bytesFromLines creates the line representation with the given endOfLine char
|
||||||
|
func (ls lines) bytesFromLines(sep []byte) []byte {
|
||||||
|
var raw []byte
|
||||||
|
for i := range ls {
|
||||||
|
raw = append(raw, ls[i].line...)
|
||||||
|
if len(ls[i].endOfLine) != 0 && sep != nil {
|
||||||
|
raw = append(raw, sep...)
|
||||||
|
} else {
|
||||||
|
raw = append(raw, ls[i].endOfLine...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return raw
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseLines constructs the lines representation of a given message
|
||||||
|
func parseLines(raw []byte) lines {
|
||||||
|
oneLine := line{raw, nil}
|
||||||
|
lines := lines{oneLine}
|
||||||
|
lines = lines.splitLine([]byte("\r\n"))
|
||||||
|
lines = lines.splitLine([]byte("\r"))
|
||||||
|
lines = lines.splitLine([]byte("\n"))
|
||||||
|
return lines
|
||||||
|
}
|
||||||
|
|
||||||
|
// splitLine uses the given endOfLine to split the given line
|
||||||
|
func (ls lines) splitLine(sep []byte) lines {
|
||||||
|
nl := lines{}
|
||||||
|
for _, l := range ls {
|
||||||
|
split := bytes.Split(l.line, sep)
|
||||||
|
if len(split) > 1 {
|
||||||
|
for i := 0; i < len(split)-1; i++ {
|
||||||
|
nl = append(nl, line{split[i], sep})
|
||||||
|
}
|
||||||
|
nl = append(nl, line{split[len(split)-1], l.endOfLine})
|
||||||
|
} else {
|
||||||
|
nl = append(nl, l)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nl
|
||||||
|
}
|
334
smime_test.go
Normal file
334
smime_test.go
Normal file
|
@ -0,0 +1,334 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/rsa"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestGet_RSA(t *testing.T) {
|
||||||
|
p := privateKeyHolder{
|
||||||
|
ecdsa: nil,
|
||||||
|
rsa: &rsa.PrivateKey{},
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.get() == nil {
|
||||||
|
t.Errorf("get() did not return the correct private key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGet_ECDSA(t *testing.T) {
|
||||||
|
p := privateKeyHolder{
|
||||||
|
ecdsa: &ecdsa.PrivateKey{},
|
||||||
|
rsa: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.get() == nil {
|
||||||
|
t.Errorf("get() did not return the correct private key")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNewSMimeWithRSA tests the newSMime method with RSA crypto material
|
||||||
|
func TestNewSMimeWithRSA(t *testing.T) {
|
||||||
|
privateKey, certificate, intermediateCertificate, err := getDummyRSACryptoMaterial()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error getting dummy crypto material: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sMime, err := newSMimeWithRSA(privateKey, certificate, intermediateCertificate)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error creating new SMime from keyPair: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sMime.privateKey.rsa != privateKey {
|
||||||
|
t.Errorf("NewSMime() did not return the same private key")
|
||||||
|
}
|
||||||
|
if sMime.certificate != certificate {
|
||||||
|
t.Errorf("NewSMime() did not return the same certificate")
|
||||||
|
}
|
||||||
|
if sMime.intermediateCertificate != intermediateCertificate {
|
||||||
|
t.Errorf("NewSMime() did not return the same intermedidate certificate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNewSMimeWithECDSA tests the newSMime method with ECDSA crypto material
|
||||||
|
func TestNewSMimeWithECDSA(t *testing.T) {
|
||||||
|
privateKey, certificate, intermediateCertificate, err := getDummyECDSACryptoMaterial()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error getting dummy crypto material: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sMime, err := newSMimeWithECDSA(privateKey, certificate, intermediateCertificate)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error creating new SMime from keyPair: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sMime.privateKey.ecdsa != privateKey {
|
||||||
|
t.Errorf("NewSMime() did not return the same private key")
|
||||||
|
}
|
||||||
|
if sMime.certificate != certificate {
|
||||||
|
t.Errorf("NewSMime() did not return the same certificate")
|
||||||
|
}
|
||||||
|
if sMime.intermediateCertificate != intermediateCertificate {
|
||||||
|
t.Errorf("NewSMime() did not return the same intermedidate certificate")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSign tests the sign method
|
||||||
|
func TestSign(t *testing.T) {
|
||||||
|
privateKey, certificate, intermediateCertificate, err := getDummyRSACryptoMaterial()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error getting dummy crypto material: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sMime, err := newSMimeWithRSA(privateKey, certificate, intermediateCertificate)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error creating new SMime from keyPair: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
message := "This is a test message"
|
||||||
|
singedMessage, err := sMime.signMessage(message)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error creating singed message: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *singedMessage == message {
|
||||||
|
t.Errorf("Sign() did not work")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPrepareMessage tests the createMessage method
|
||||||
|
func TestPrepareMessage(t *testing.T) {
|
||||||
|
privateKey, certificate, intermediateCertificate, err := getDummyRSACryptoMaterial()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error getting dummy crypto material: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sMime, err := newSMimeWithRSA(privateKey, certificate, intermediateCertificate)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error creating new SMime from keyPair: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
encoding := EncodingB64
|
||||||
|
contentType := TypeTextPlain
|
||||||
|
charset := CharsetUTF8
|
||||||
|
body := []byte("This is the body!")
|
||||||
|
result, err := sMime.prepareMessage(encoding, contentType, charset, body)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error preparing message: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(*result, encoding.String()) {
|
||||||
|
t.Errorf("prepareMessage() did not return the correct encoding")
|
||||||
|
}
|
||||||
|
if !strings.Contains(*result, contentType.String()) {
|
||||||
|
t.Errorf("prepareMessage() did not return the correct contentType")
|
||||||
|
}
|
||||||
|
if !strings.Contains(*result, string(body)) {
|
||||||
|
t.Errorf("prepareMessage() did not return the correct body")
|
||||||
|
}
|
||||||
|
if *result != fmt.Sprintf("Content-Transfer-Encoding: %s\r\nContent-Type: %s; charset=%s\r\n\r\n%s", encoding, contentType, charset, string(body)) {
|
||||||
|
t.Errorf("prepareMessage() did not sucessfully create the message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestPrepareMessage_QuotedPrintable tests the prepareMessage method with quoted printable encoding
|
||||||
|
func TestPrepareMessage_QuotedPrintable(t *testing.T) {
|
||||||
|
privateKey, certificate, intermediateCertificate, err := getDummyRSACryptoMaterial()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error getting dummy crypto material: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sMime, err := newSMimeWithRSA(privateKey, certificate, intermediateCertificate)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error creating new SMime from keyPair: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
body := "This is the body with special chars like äöü ÄÖÜ ß!"
|
||||||
|
quotedPrintableBody := "This is the body with special chars like =C3=A4=C3=B6=C3=BC =C3=84=C3=96=C3=\r\n=9C =C3=9F!"
|
||||||
|
encoding := EncodingQP
|
||||||
|
contentType := TypeTextPlain
|
||||||
|
charset := CharsetUTF8
|
||||||
|
result, err := sMime.prepareMessage(encoding, contentType, charset, []byte(body))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error preparing message: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(*result, encoding.String()) {
|
||||||
|
t.Errorf("prepareMessage() did not return the correct encoding")
|
||||||
|
}
|
||||||
|
if !strings.Contains(*result, contentType.String()) {
|
||||||
|
t.Errorf("prepareMessage() did not return the correct contentType")
|
||||||
|
}
|
||||||
|
if !strings.Contains(*result, quotedPrintableBody) {
|
||||||
|
t.Errorf("prepareMessage() did not return the correct body")
|
||||||
|
}
|
||||||
|
if *result != fmt.Sprintf("Content-Transfer-Encoding: %s\r\nContent-Type: %s; charset=%s\r\n\r\n%s", encoding, contentType, charset, quotedPrintableBody) {
|
||||||
|
t.Errorf("prepareMessage() did not sucessfully create the message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEncodeMessage tests the TestEncodeMessage method without any encoding
|
||||||
|
func TestEncodeMessage(t *testing.T) {
|
||||||
|
body := "This is the body with special chars like äöü ÄÖÜ ß!"
|
||||||
|
encoding := EncodingUSASCII
|
||||||
|
|
||||||
|
privateKey, certificate, intermediateCertificate, err := getDummyRSACryptoMaterial()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error getting dummy crypto material: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sMime, err := newSMimeWithRSA(privateKey, certificate, intermediateCertificate)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error creating new SMime from keyPair: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := sMime.encodeMessage(encoding, body)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error preparing message: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *result != body {
|
||||||
|
t.Errorf("encodeMessage() did not return the correct encoded message: %s", *result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEncodeMessage_QuotedPrintable tests the TestEncodeMessage method with quoted printable body
|
||||||
|
func TestEncodeMessage_QuotedPrintable(t *testing.T) {
|
||||||
|
body := "This is the body with special chars like äöü ÄÖÜ ß!"
|
||||||
|
quotedPrintableBody := "This is the body with special chars like =C3=A4=C3=B6=C3=BC =C3=84=C3=96=C3=\r\n=9C =C3=9F!"
|
||||||
|
encoding := EncodingQP
|
||||||
|
|
||||||
|
privateKey, certificate, intermediateCertificate, err := getDummyRSACryptoMaterial()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error getting dummy crypto material: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sMime, err := newSMimeWithRSA(privateKey, certificate, intermediateCertificate)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error creating new SMime from keyPair: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := sMime.encodeMessage(encoding, body)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error preparing message: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *result != quotedPrintableBody {
|
||||||
|
t.Errorf("encodeMessage() did not return the correct encoded message: %s", *result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEncodeToPEM tests the encodeToPEM method
|
||||||
|
func TestEncodeToPEM(t *testing.T) {
|
||||||
|
message := []byte("This is a test message")
|
||||||
|
|
||||||
|
pemMessage, err := encodeToPEM(message)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Error encoding message: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
base64Encoded := base64.StdEncoding.EncodeToString(message)
|
||||||
|
if *pemMessage != base64Encoded {
|
||||||
|
t.Errorf("encodeToPEM() did not work")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBytesFromLines tests the bytesFromLines method
|
||||||
|
func TestBytesFromLines(t *testing.T) {
|
||||||
|
ls := lines{
|
||||||
|
{line: []byte("Hello"), endOfLine: []byte("\n")},
|
||||||
|
{line: []byte("World"), endOfLine: []byte("\n")},
|
||||||
|
}
|
||||||
|
expected := []byte("Hello\nWorld\n")
|
||||||
|
|
||||||
|
result := ls.bytesFromLines([]byte("\n"))
|
||||||
|
if !bytes.Equal(result, expected) {
|
||||||
|
t.Errorf("Expected %s, but got %s", expected, result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FuzzBytesFromLines tests the bytesFromLines method with fuzzing
|
||||||
|
func FuzzBytesFromLines(f *testing.F) {
|
||||||
|
f.Add([]byte("Hello"), []byte("\n"))
|
||||||
|
f.Fuzz(func(t *testing.T, lineData, sep []byte) {
|
||||||
|
ls := lines{
|
||||||
|
{line: lineData, endOfLine: sep},
|
||||||
|
}
|
||||||
|
_ = ls.bytesFromLines(sep)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestParseLines tests the parseLines method
|
||||||
|
func TestParseLines(t *testing.T) {
|
||||||
|
input := []byte("Hello\r\nWorld\nHello\rWorld")
|
||||||
|
expected := lines{
|
||||||
|
{line: []byte("Hello"), endOfLine: []byte("\r\n")},
|
||||||
|
{line: []byte("World"), endOfLine: []byte("\n")},
|
||||||
|
{line: []byte("Hello"), endOfLine: []byte("\r")},
|
||||||
|
{line: []byte("World"), endOfLine: []byte("")},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := parseLines(input)
|
||||||
|
if len(result) != len(expected) {
|
||||||
|
t.Errorf("Expected %d lines, but got %d", len(expected), len(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range result {
|
||||||
|
if !bytes.Equal(result[i].line, expected[i].line) || !bytes.Equal(result[i].endOfLine, expected[i].endOfLine) {
|
||||||
|
t.Errorf("Line %d mismatch. Expected line: %s, endOfLine: %s, got line: %s, endOfLine: %s",
|
||||||
|
i, expected[i].line, expected[i].endOfLine, result[i].line, result[i].endOfLine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FuzzParseLines tests the parseLines method with fuzzing
|
||||||
|
func FuzzParseLines(f *testing.F) {
|
||||||
|
f.Add([]byte("Hello\nWorld\r\nAnother\rLine"))
|
||||||
|
f.Fuzz(func(t *testing.T, input []byte) {
|
||||||
|
_ = parseLines(input)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSplitLine tests the splitLine method
|
||||||
|
func TestSplitLine(t *testing.T) {
|
||||||
|
ls := lines{
|
||||||
|
{line: []byte("Hello\r\nWorld\r\nAnotherLine"), endOfLine: []byte("")},
|
||||||
|
}
|
||||||
|
expected := lines{
|
||||||
|
{line: []byte("Hello"), endOfLine: []byte("\r\n")},
|
||||||
|
{line: []byte("World"), endOfLine: []byte("\r\n")},
|
||||||
|
{line: []byte("AnotherLine"), endOfLine: []byte("")},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := ls.splitLine([]byte("\r\n"))
|
||||||
|
if len(result) != len(expected) {
|
||||||
|
t.Errorf("Expected %d lines, but got %d", len(expected), len(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range result {
|
||||||
|
if !bytes.Equal(result[i].line, expected[i].line) || !bytes.Equal(result[i].endOfLine, expected[i].endOfLine) {
|
||||||
|
t.Errorf("Line %d mismatch. Expected line: %s, endOfLine: %s, got line: %s, endOfLine: %s",
|
||||||
|
i, expected[i].line, expected[i].endOfLine, result[i].line, result[i].endOfLine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FuzzSplitLine tests the parseLsplitLineines method with fuzzing
|
||||||
|
func FuzzSplitLine(f *testing.F) {
|
||||||
|
f.Add([]byte("Hello\r\nWorld"), []byte("\r\n"))
|
||||||
|
f.Fuzz(func(t *testing.T, input, sep []byte) {
|
||||||
|
ls := lines{
|
||||||
|
{line: input, endOfLine: []byte("")},
|
||||||
|
}
|
||||||
|
_ = ls.splitLine(sep)
|
||||||
|
})
|
||||||
|
}
|
73
util_test.go
Normal file
73
util_test.go
Normal file
|
@ -0,0 +1,73 @@
|
||||||
|
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/ecdsa"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
certRSAFilePath = "dummy-chain-cert-rsa.pem"
|
||||||
|
keyRSAFilePath = "dummy-child-key-rsa.pem"
|
||||||
|
|
||||||
|
certECDSAFilePath = "dummy-chain-cert-ecdsa.pem"
|
||||||
|
keyECDSAFilePath = "dummy-child-key-ecdsa.pem"
|
||||||
|
)
|
||||||
|
|
||||||
|
// getDummyRSACryptoMaterial loads a certificate (RSA), the associated private key and certificate (RSA) is loaded from local disk for testing purposes
|
||||||
|
func getDummyRSACryptoMaterial() (*rsa.PrivateKey, *x509.Certificate, *x509.Certificate, error) {
|
||||||
|
keyPair, err := tls.LoadX509KeyPair(certRSAFilePath, keyRSAFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKey := keyPair.PrivateKey.(*rsa.PrivateKey)
|
||||||
|
|
||||||
|
certificate, err := x509.ParseCertificate(keyPair.Certificate[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
intermediateCertificate, err := x509.ParseCertificate(keyPair.Certificate[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return privateKey, certificate, intermediateCertificate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDummyECDSACryptoMaterial loads a certificate (ECDSA), the associated private key and certificate (ECDSA) is loaded from local disk for testing purposes
|
||||||
|
func getDummyECDSACryptoMaterial() (*ecdsa.PrivateKey, *x509.Certificate, *x509.Certificate, error) {
|
||||||
|
keyPair, err := tls.LoadX509KeyPair(certECDSAFilePath, keyECDSAFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
privateKey := keyPair.PrivateKey.(*ecdsa.PrivateKey)
|
||||||
|
|
||||||
|
certificate, err := x509.ParseCertificate(keyPair.Certificate[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
intermediateCertificate, err := x509.ParseCertificate(keyPair.Certificate[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return privateKey, certificate, intermediateCertificate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDummyKeyPairTLS loads a certificate (ECDSA) as *tls.Certificate, the associated private key and certificate (ECDSA) is loaded from local disk for testing purposes
|
||||||
|
func getDummyKeyPairTLS() (*tls.Certificate, error) {
|
||||||
|
keyPair, err := tls.LoadX509KeyPair(certECDSAFilePath, keyECDSAFilePath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &keyPair, err
|
||||||
|
}
|
Loading…
Reference in a new issue