This commit is contained in:
Michael Fuchs 2024-10-11 17:22:52 +00:00 committed by GitHub
commit c202e8d20c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 994 additions and 17 deletions

View file

@ -59,6 +59,7 @@ Here are some highlights of go-mail's featureset:
* [X] Custom error types for delivery errors
* [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] S/MIME singed messages
go-mail works like a programatic email client and provides lots of methods and functionalities you would consider
standard in a MUA.

62
dummy-chain-cert.pem Normal file
View file

@ -0,0 +1,62 @@
-----BEGIN CERTIFICATE-----
MIIFWjCCA0KgAwIBAgIUAi7P4JOR4g8b5DMERUtZQEtw+igwDQYJKoZIhvcNAQEL
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDA5MjYxNDU0MDlaFw0yNTA5
MjYxNDU0MDlaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB
AQUAA4ICDwAwggIKAoICAQCiLh0JJTRRBhmUyiMKALHtTOK7T20Bwy+fG0SO6RlB
c+hSuuX/n6znXcNgOBlQ2Gg3+p1on/bmcKnGN/SCiVBLpROiwxg3blQbZ7B7Jors
/MopGk0LIBOXHPtAuYbF4J6ND5Ol6sgeGjMnomwRjZlfeuBlHY345MqvcwH/lPhO
lKme+tWD+bsFh08NGS+3NdQGP6dA2bRVrPXhLXStHEmqfKO9EMVLWv+77tYhZESD
6XgnA8pWjbdr9jajCsrQWrCG3jqHtzHNxtwf7xfRwwgoUhLEvue6SBVokZGVmDhv
WdRt2sjtLcWWJCI3p7M+NXRt5qf6iu24wLBdzIDuWfgooWu5vBzNZjSTh2if6R1a
s9BdwASwy1n2HMvqpzgA+f/rXDFbvVc7WIKiuGzfWApBrL0qTCQBuSyepH0G4rQ2
sJtI5U8QOKBO76nQJq5WDQgefBX4GDI8aJ6qX+teQq1AqERUmLWx4WlTwxSo5X+d
1jY9I8f61CRKVfIRgMvAZUhm2h6RnoVIgq7G7W3HdC3RT/f758njI7oIv5bhiyqp
gyKr3cYhmn0enjP+YtjY85m51q005qzRLfaTYiwMR4qyJW4ZOEPntPs20CD+e+Pi
2JLONpRdcsSrkqZusVjm5PFy2e5RyNFXTupUH2KVrgTRHL3GG2KWF5PmBdkhQfGG
1QIDAQABo0IwQDAdBgNVHQ4EFgQUS8+HouVQ94SdgI3kV3L8Jm53P6YwHwYDVR0j
BBgwFoAUY1u7KerT8m01BvAg77PUaot2S0EwDQYJKoZIhvcNAQELBQADggIBAE14
YBa/stYwrsy/1iQ44NeQyYMPMdOC8TI1xrbW/9u1FllipECnFEGDK1N6mZ7xDEfG
un5dQ3jXQ7156Ge672374yUsN7FQ37mTyZos3Q2N/mOpVOnYJt5mIukx2MXBU3r3
UP1Jpnf9rB4kdtWXa7b1CSTkM4kraige3wZhPELwESnm4t8C34MIzHBWPbHpft05
WheDv9Zizfw+0pbJ+WNGnHF4PjR/wq9ymkLf89cqsbS9mOdPpWva8i0e7pqKnxzo
iz2ueQB4Z2Tbgp0G9ResA+2Zxk1iIQPbhtqNUZv6ROPiLAWdiVRysFJJf/19V+nZ
LIC0xw+amF6P51/fA95EGqElO4OLJTIGY27H761g7+FhTwfryLMHKknSxcfk7xoq
BMyBr7ARYnmpjee7yKOBUgSdpxb6YUcdGZwjCUIiIHlBII83DzcNILa5QvUkzMCh
xHYmPvvftJOjF8hwMfjA9MDFML9yWVm+CBNraNPh25U5uMOuIuyUBtSB5yEdPRhY
BJGrZEew0lLAWAqqASmGPDaWNBaA0HYqO70g4IyqBwIGNnaHSLVr/vT23BFRMyXf
wh5mtJmyyR3+c0po3vDX39mkIAZ2gZWprWa3Jw0dVs6cEujcVNdqeZlQw0RCa9wm
xioBxb1md2AplUQ2fG/KHnu2ZuxHA6MNYcwMJNgv
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIUKU+ta2rnJE/79L2Uxg4vFoF0RxYwDQYJKoZIhvcNAQEL
BQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM
GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yNDA5MjYxNDUzMzlaFw0zNDA5
MjQxNDUzMzlaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw
HwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggIiMA0GCSqGSIb3DQEB
AQUAA4ICDwAwggIKAoICAQDSOuYqxNsP4Gm/EDauon/SAVWj8PIKFB1OpuguHm13
b2l2G6hRuNnAmR6ewP22H/YoyYz1qRchH2qw1uizwnnSS0OY74CsJhd0PV6f4XRR
Y+6PotGDPu1fJJM4XI3HjWGdBkJSawZNWjP1dQRJPUHNRttSOrPsG3XT7TjfLjoK
jOelTqgwHUGE2n0AtQP7ZFQVn7LLBrukve8zMgivwEL1JFSlKppWFf0SUgpmQVE6
3jTAQPPrI/B5z5Ys1j2jv7mJt3/UATcTGmvPTNv94SUrO6nC3TJxKHtR30MALteo
EgH/s2O0Ax44iDENgm9p6eb+GCyTWS/eHBAJ6SU76PRymiE57/0GOqyYewuEOuIU
FYd6+gglCMe+ayfhI20njHP6RTiQpRjFy+DM8+bkcS89q0sfFSFHR5oFNbAgUgyI
bGiaWb+DmUCwSFnS0HusSU2AECqzuwiyObD3rkoqBQMj+xl6SnJU6TTcB+WD/Z5G
tqu1zTMXpo3VRts3AQSGUuSaqbeG/S+38LX+fbjeTLa6SEGJfB7/H2s64vCO/0hR
M0KEXAaTyjx0PnNKYSlCIJyA9lYea21oByNc31tkUXQjmUQpSXYayrDwzR2JAXVY
rFJLNu1Q6sZ0WqT9fj06oTas7g1g3gcl18tIapeael2jJohth0RizBxKYuLYc6Pv
1QIDAQABo1MwUTAdBgNVHQ4EFgQUY1u7KerT8m01BvAg77PUaot2S0EwHwYDVR0j
BBgwFoAUY1u7KerT8m01BvAg77PUaot2S0EwDwYDVR0TAQH/BAUwAwEB/zANBgkq
hkiG9w0BAQsFAAOCAgEAB8BpmI3v2cNlXUjhf+rldaqDfq/IJ7Ri23kCBJW4VaoW
c0UrtvLC+K5m61I1iWSUYEK85/bPw2K5dn1e8w3Q2J460Yvc7/ZT7mucZlXQxfl3
V7yqqQI7OMsY6FochYUL3+c32WQg5jllsLPlHAHBJlagf3uEqmVrvSExHNBQOVyE
/cs1i9DcTJF2A8JNPKilIObvRT103Qp2eFnW+EY9OUBb+TdQvPjxroLfK1SuOAe6
bLPBxdgvA/0raHuXeDTNsNRICIU1X5eBfZwCXKe9lRVJpIsKTYeHDN/rEmfTtehB
vz8/KkCWqwPDn/YFkNAdg3TRjqW4oW2wZ+XqbTlR2qA7szE7oMAfHxNkintxMnNm
vD2/AAP6RUw16HZk0najFWPIG9gc+O1gSks6hwn9JilAPy8mn40H2D7cedU6Ew+T
CQ02+dw2+2FLKYr1eiYPlIELsAu8kmbrjwvwy2sCf3L4fxLtPRqXFuXB2Uer9zvy
tn+RK5hJkKo/YY37I9Y9x57rpCqUfFIeYWBub07x1620ujRkL1pJPxfRNBfyh42t
beuk/XQGIvPcIkbPnmsb4gGaiRMuw+mZ7isJDoQwHUmfqL1EpOYb5mLYHkIqKaCz
8t8wTdkimIVIFSxedy7cJCCWdQ/BCyTJoQpXD69PLPzxEi/YK9pB9S8qBtfefu4=
-----END CERTIFICATE-----

View file

@ -0,0 +1,3 @@
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
//
// SPDX-License-Identifier: MIT

52
dummy-child-key.pem Normal file
View file

@ -0,0 +1,52 @@
-----BEGIN PRIVATE KEY-----
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
S++xGnGHEUSEl7cWnO8k0RyJzzc=
-----END PRIVATE KEY-----

View file

@ -0,0 +1,3 @@
// SPDX-FileCopyrightText: Copyright (c) 2022-2024 The go-mail Authors
//
// SPDX-License-Identifier: MIT

View file

@ -171,17 +171,20 @@ const (
// TypeTextPlain represents the MIME type for plain text content.
TypeTextPlain ContentType = "text/plain"
// typeSMimeSigned represents the MIME type for S/MIME singed messages.
typeSMimeSigned ContentType = `application/pkcs7-signature; name="smime.p7s"`
)
const (
// MIMEAlternative MIMEType represents a MIME multipart/alternative type, used for emails with multiple versions.
MIMEAlternative MIMEType = "alternative"
// MIMEMixed MIMEType represents a MIME multipart/mixed type used for emails containing different types of content.
MIMEMixed MIMEType = "mixed"
// MIMERelated MIMEType represents a MIME multipart/related type, used for emails with related content entities.
MIMERelated MIMEType = "related"
// 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.
@ -219,3 +222,8 @@ func (c ContentType) String() string {
func (e Encoding) String() string {
return string(e)
}
// String is a standard method to convert an MIMEType into a printable format
func (e MIMEType) String() string {
return string(e)
}

View file

@ -61,6 +61,11 @@ func TestContentType_String(t *testing.T) {
"ContentType: application/pgp-encrypted", TypePGPEncrypted,
"application/pgp-encrypted",
},
{
"ContentType: pkcs7-signature", typeSMimeSigned,
`application/pkcs7-signature; name="smime.p7s"`,
},
}
for _, tt := range tests {
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())
}
})
}
}

1
go.mod
View file

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

4
go.sum
View file

@ -20,6 +20,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI=
go.mozilla.org/pkcs7 v0.9.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@ -63,4 +65,4 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

71
msg.go
View file

@ -7,6 +7,8 @@ package mail
import (
"bytes"
"context"
"crypto/rsa"
"crypto/x509"
"embed"
"errors"
"fmt"
@ -24,7 +26,7 @@ import (
)
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")
// ErrNoRcptAddresses indicates that no recipient addresses have been set.
@ -144,6 +146,9 @@ type Msg struct {
//
// This can be useful in scenarios where headers are conditionally passed based on receipt - i. e. SMTP proxies.
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.
@ -333,8 +338,16 @@ func WithNoDefaultUserAgent() MsgOption {
}
}
// SetCharset sets or overrides the currently set encoding charset of the Msg.
//
// SignWithSMime configures the Msg to be signed with S/MIME
func (m *Msg) SignWithSMime(privateKey *rsa.PrivateKey, certificate *x509.Certificate, intermediateCertificate *x509.Certificate) error {
sMime, err := newSMime(privateKey, certificate, intermediateCertificate)
if err != nil {
return err
}
m.sMime = sMime
return nil
}
// This method allows you to specify a character set for the email message. The charset is
// important for ensuring that the content of the message is correctly interpreted by
// mail clients. Common charset values include UTF-8, ISO-8859-1, and others. If a charset
@ -2115,6 +2128,38 @@ func (m *Msg) applyMiddlewares(msg *Msg) *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 := m.sMime.prepareMessage(encoding, contentType, charSet, body)
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.
//
// This method writes the email message, including its headers, body, and attachments, to the provided
@ -2132,7 +2177,18 @@ func (m *Msg) applyMiddlewares(msg *Msg) *Msg {
// - https://datatracker.ietf.org/doc/html/rfc5322
func (m *Msg) WriteTo(writer io.Writer) (int64, error) {
mw := &msgWriter{writer: writer, charset: m.charset, encoder: m.encoder}
mw.writeMsg(m.applyMiddlewares(m))
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
}
@ -2496,7 +2552,7 @@ func (m *Msg) hasAlt() bool {
count++
}
}
return count > 1 && m.pgptype == 0
return count > 1 && m.pgptype == 0 && !m.hasSMime()
}
// hasMixed returns true if the Msg has mixed parts.
@ -2514,6 +2570,11 @@ func (m *Msg) hasMixed() bool {
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.
//
// This method checks whether the message contains related parts, such as inline embedded files

View file

@ -1907,6 +1907,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 := getDummyCryptoMaterial()
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.SignWithSMime(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 := getDummyCryptoMaterial()
if err != nil {
t.Errorf("failed to laod dummy crypto material. Cause: %v", err)
}
m := NewMsg()
if err := m.SignWithSMime(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")
}
}
// TestMsg_hasRelated tests the hasRelated() method of the Msg
func TestMsg_hasRelated(t *testing.T) {
m := NewMsg()
@ -1974,6 +2007,70 @@ func TestMsg_WriteToSkipMiddleware(t *testing.T) {
}
}
// TestMsg_WriteToWithSMIME tests the WriteTo() method of the Msg
func TestMsg_WriteToWithSMIME(t *testing.T) {
privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial()
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.SignWithSMime(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")
}
}
// TestMsg_WriteTo_fails tests the WriteTo() method of the Msg but with a failing body writer function
func TestMsg_WriteTo_fails(t *testing.T) {
m := NewMsg()
@ -3246,6 +3343,62 @@ func TestNewMsgWithNoDefaultUserAgent(t *testing.T) {
}
}
// TestSignWithSMime_ValidKeyPair tests WithSMimeSinging with given key pair
func TestSignWithSMime_ValidKeyPair(t *testing.T) {
privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial()
if err != nil {
t.Errorf("failed to laod dummy crypto material. Cause: %v", err)
}
m := NewMsg()
if err := m.SignWithSMime(privateKey, certificate, intermediateCertificate); err != nil {
t.Errorf("failed to set sMime. Cause: %v", err)
}
if m.sMime.privateKey == nil {
t.Errorf("WithSMimeSinging() - no private key is given")
}
if m.sMime.certificate == nil {
t.Errorf("WithSMimeSinging() - no certificate is given")
}
}
// TestSignWithSMime_InvalidPrivateKey tests WithSMimeSinging with given invalid private key
func TestSignWithSMime_InvalidPrivateKey(t *testing.T) {
m := NewMsg()
err := m.SignWithSMime(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 := getDummyCryptoMaterial()
if err != nil {
t.Errorf("failed to laod dummy crypto material. Cause: %v", err)
}
m := NewMsg()
err = m.SignWithSMime(privateKey, nil, nil)
if !errors.Is(err, ErrInvalidCertificate) {
t.Errorf("failed to pre-check SignWithSMime method values correctly: %s", err)
}
}
// TestSignWithSMime_InvalidIntermediateCertificate tests WithSMimeSinging with given invalid intermediate certificate
func TestSignWithSMime_InvalidIntermediateCertificate(t *testing.T) {
privateKey, certificate, _, err := getDummyCryptoMaterial()
if err != nil {
t.Errorf("failed to laod dummy crypto material. Cause: %v", err)
}
m := NewMsg()
err = m.SignWithSMime(privateKey, certificate, nil)
if !errors.Is(err, ErrInvalidIntermediateCertificate) {
t.Errorf("failed to pre-check SignWithSMime method values correctly: %s", err)
}
}
// Fuzzing tests
func FuzzMsg_Subject(f *testing.F) {
f.Add("Testsubject")
@ -3273,3 +3426,84 @@ func FuzzMsg_From(f *testing.F) {
m.Reset()
})
}
// TestMsg_createSignaturePart tests the Msg.createSignaturePart method
func TestMsg_createSignaturePart(t *testing.T) {
privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial()
if err != nil {
t.Errorf("failed to laod dummy crypto material. Cause: %v", err)
}
m := NewMsg()
if err := m.SignWithSMime(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 := getDummyCryptoMaterial()
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.SignWithSMime(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())
}
}

View file

@ -128,6 +128,10 @@ func (mw *msgWriter) writeMsg(msg *Msg) {
}
}
if msg.hasSMime() {
mw.startMP(MIMESMime, msg.boundary)
mw.writeString(DoubleNewLine)
}
if msg.hasMixed() {
mw.startMP(MIMEMixed, msg.boundary)
mw.writeString(DoubleNewLine)
@ -174,6 +178,10 @@ func (mw *msgWriter) writeMsg(msg *Msg) {
if msg.hasMixed() {
mw.stopMP()
}
if msg.hasSMime() {
mw.stopMP()
}
}
// 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 {
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() == "" {
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()
if mw.depth == 0 {
mw.writeHeader(HeaderContentType, contentType)
@ -363,7 +376,7 @@ func (mw *msgWriter) writePart(part *Part, charset Charset) {
mimeHeader.Add(string(HeaderContentTransferEnc), contentTransferEnc)
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.
@ -438,7 +451,8 @@ func (mw *msgWriter) writeHeader(key Header, values ...string) {
// Parameters:
// - writeFunc: A function that writes the body content to the given io.Writer.
// - encoding: The encoding type to use when writing the content (e.g., base64, quoted-printable).
func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encoding Encoding) {
// - 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 encodedWriter io.WriteCloser
var n int64
@ -453,12 +467,11 @@ func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encodin
lineBreaker := Base64LineBreaker{}
lineBreaker.out = &writeBuffer
switch encoding {
case EncodingQP:
if encoding == EncodingQP {
encodedWriter = quotedprintable.NewWriter(&writeBuffer)
case EncodingB64:
} else if encoding == EncodingB64 && !singingWithSMime {
encodedWriter = base64.NewEncoder(base64.StdEncoding, &lineBreaker)
case NoEncoding:
} else if encoding == NoEncoding || singingWithSMime {
_, err = writeFunc(&writeBuffer)
if err != nil {
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
}
return
default:
} else {
encodedWriter = quotedprintable.NewWriter(writer)
}

View file

@ -154,3 +154,42 @@ func TestMsgWriter_writeMsg_PGP(t *testing.T) {
t.Errorf("writeMsg failed. Expected PGP encoding header but didn't find it in message output")
}
}
// 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 := getDummyCryptoMaterial()
if err != nil {
t.Errorf("failed to laod dummy crypto material. Cause: %v", err)
}
m := NewMsg()
if err := m.SignWithSMime(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
View file

@ -24,6 +24,7 @@ type Part struct {
encoding Encoding
isDeleted bool
writeFunc func(io.Writer) (int64, error)
smime bool
}
// 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
}
// 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.
//
// 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
}
// 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.
//
// 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
}
}
// WithSMimeSinging overrides the flag for signing the Part with S/MIME
func WithSMimeSinging() PartOption {
return func(p *Part) {
p.smime = true
}
}

View file

@ -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
func TestPart_SetContentType(t *testing.T) {
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
func TestPart_SetWriteFunc(t *testing.T) {
c := "This is a test with ümläutß"

174
smime.go Normal file
View file

@ -0,0 +1,174 @@
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
//
// SPDX-License-Identifier: MIT
package mail
import (
"bytes"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"strings"
"go.mozilla.org/pkcs7"
)
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")
// ErrInvalidIntermediateCertificate should be used if the intermediate certificate is invalid
ErrInvalidIntermediateCertificate = errors.New("invalid intermediate certificate")
// ErrCouldNotInitialize should be used if the signed data could not initialize
ErrCouldNotInitialize = errors.New("could not initialize signed data")
// ErrCouldNotAddSigner should be used if the signer could not be added
ErrCouldNotAddSigner = errors.New("could not add signer message")
// ErrCouldNotFinishSigning should be used if the signing could not be finished
ErrCouldNotFinishSigning = errors.New("could not finish signing")
// ErrCouldNoEncodeToPEM should be used if the signature could not be encoded to PEM
ErrCouldNoEncodeToPEM = errors.New("could not encode to PEM")
)
// SMime is used to sign messages with S/MIME
type SMime struct {
privateKey *rsa.PrivateKey
certificate *x509.Certificate
intermediateCertificate *x509.Certificate
}
// NewSMime construct a new instance of SMime with provided parameters
// privateKey as *rsa.PrivateKey
// certificate as *x509.Certificate
// intermediateCertificate as *x509.Certificate
func newSMime(privateKey *rsa.PrivateKey, certificate *x509.Certificate, intermediateCertificate *x509.Certificate) (*SMime, error) {
if privateKey == nil {
return nil, ErrInvalidPrivateKey
}
if certificate == nil {
return nil, ErrInvalidCertificate
}
if intermediateCertificate == nil {
return nil, ErrInvalidIntermediateCertificate
}
return &SMime{
privateKey: 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 := pkcs7.NewSignedData(toBeSigned)
signedData.SetDigestAlgorithm(pkcs7.OIDDigestAlgorithmSHA256)
if err != nil {
return nil, ErrCouldNotInitialize
}
if err = signedData.AddSignerChain(sm.certificate, sm.privateKey, []*x509.Certificate{sm.intermediateCertificate}, pkcs7.SignerInfoConfig{}); err != nil {
return nil, ErrCouldNotAddSigner
}
signedData.Detach()
signatureDER, err := signedData.Finish()
if err != nil {
return nil, ErrCouldNotFinishSigning
}
pemMsg, err := encodeToPEM(signatureDER)
if err != nil {
return nil, ErrCouldNoEncodeToPEM
}
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 {
return fmt.Sprintf("Content-Transfer-Encoding: %v\r\nContent-Type: %v; charset=%v\r\n\r\n%v", encoding, contentType, charset, string(body))
}
// 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
}

197
smime_test.go Normal file
View file

@ -0,0 +1,197 @@
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
//
// SPDX-License-Identifier: MIT
package mail
import (
"bytes"
"encoding/base64"
"fmt"
"strings"
"testing"
)
// TestNewSMime tests the newSMime method
func TestNewSMime(t *testing.T) {
privateKey, certificate, intermediateCertificate, err := getDummyCryptoMaterial()
if err != nil {
t.Errorf("Error getting dummy crypto material: %s", err)
}
sMime, err := newSMime(privateKey, certificate, intermediateCertificate)
if err != nil {
t.Errorf("Error creating new SMime from keyPair: %s", err)
}
if sMime.privateKey != 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 := getDummyCryptoMaterial()
if err != nil {
t.Errorf("Error getting dummy crypto material: %s", err)
}
sMime, err := newSMime(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 := getDummyCryptoMaterial()
if err != nil {
t.Errorf("Error getting dummy crypto material: %s", err)
}
sMime, err := newSMime(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 := sMime.prepareMessage(encoding, contentType, charset, body)
if !strings.Contains(result, encoding.String()) {
t.Errorf("createMessage() did not return the correct encoding")
}
if !strings.Contains(result, contentType.String()) {
t.Errorf("createMessage() did not return the correct contentType")
}
if !strings.Contains(result, string(body)) {
t.Errorf("createMessage() 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("createMessage() did not sucessfully create the message")
}
}
// 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)
})
}

38
util_test.go Normal file
View file

@ -0,0 +1,38 @@
// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors
//
// SPDX-License-Identifier: MIT
package mail
import (
"crypto/rsa"
"crypto/tls"
"crypto/x509"
)
const (
certFilePath = "dummy-chain-cert.pem"
keyFilePath = "dummy-child-key.pem"
)
// getDummyCryptoMaterial loads a certificate and a private key form local disk for testing purposes
func getDummyCryptoMaterial() (*rsa.PrivateKey, *x509.Certificate, *x509.Certificate, error) {
keyPair, err := tls.LoadX509KeyPair(certFilePath, keyFilePath)
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
}