Compare commits

...

35 commits

Author SHA1 Message Date
Michael Fuchs
3bb04d58b3
Merge cc4c5bfd04 into c47f08dc7f 2024-10-25 20:11:03 +02:00
theexiile1305
cc4c5bfd04
fix: tests 2024-10-17 16:21:15 +02:00
theexiile1305
4b21cc617b
fix: tests 2024-10-17 16:14:04 +02:00
theexiile1305
4a3dbddaff
chore: formatting and using another types as deprecated ones 2024-10-17 16:06:53 +02:00
theexiile1305
978a01eaf6
feat: also added ecdsa crypto key material 2024-10-17 15:49:05 +02:00
theexiile1305
efd8aa93b6
feat: migrated resources from external repo to remove external dependency 2024-10-17 15:48:42 +02:00
theexiile1305
128b2bd32e
chore: license 2024-10-11 19:22:47 +02:00
theexiile1305
148e9d8e1c
chore: gofumpt 2024-10-11 19:21:34 +02:00
theexiile1305
df68003f24
feat: add license information 2024-10-11 19:16:42 +02:00
theexiile1305
f6bda99464
feat: transfer configuration of intermediate certificate to caller of the library 2024-10-11 18:43:04 +02:00
theexiile1305
3f2ba4822c
feat: add support of s/mime singing 2024-10-11 17:26:48 +02:00
theexiile1305
c45aec89e9
feat: add parent certificates 2024-10-11 17:26:22 +02:00
Michael Fuchs
7f7bf80e39
Merge branch 'main' into main 2024-10-09 16:29:21 +02:00
theexiile1305
12076cf64a
feat: last tests 2024-10-09 16:15:23 +02:00
theexiile1305
43ba8e3af2
feat: improved tests 2024-10-09 15:45:22 +02:00
theexiile1305
5913fc1540
feat: improved tests 2024-10-09 15:40:05 +02:00
theexiile1305
6ea0974156
feat: improved tests 2024-10-09 14:43:43 +02:00
theexiile1305
2c2ee4c1fb
feat: begin implementation of tests 2024-10-09 13:53:31 +02:00
theexiile1305
4700691380
fix: detached signature is now used 2024-10-09 13:53:15 +02:00
theexiile1305
b4370ded12
fix: micalg part of content-type 2024-10-09 12:10:03 +02:00
theexiile1305
46cf2ed498
fix: part boundray and message encoding 2024-09-29 16:46:30 +02:00
theexiile1305
79f22fb722
feat: specific error if certificate is invalid 2024-09-26 17:28:24 +02:00
theexiile1305
4b60557518
fix: failing tests 2024-09-26 17:14:42 +02:00
theexiile1305
9bdb741e05
feat: begin implementation of tests 2024-09-26 17:08:38 +02:00
theexiile1305
2368113872
feat: implementation of tests 2024-09-26 16:43:58 +02:00
theexiile1305
12edb2724b
feat: implementation of S/MIME signing without tests 2024-09-26 16:34:55 +02:00
theexiile1305
3154649420
feat: add flag smime for indicating that the part should be marked for singing with S/MIME 2024-09-26 11:37:53 +02:00
theexiile1305
65b9fd07da
feat: remove unused content disposition 2024-09-26 11:32:27 +02:00
theexiile1305
894936092e
fixing test 2024-09-18 14:35:40 +02:00
theexiile1305
41a81966d8
remove new line 2024-09-18 14:28:55 +02:00
theexiile1305
94942ed383
implementation of S/MIME singing without tests of type smime 2024-09-18 14:23:26 +02:00
theexiile1305
e0a59dba6d
introduced content disposition in part 2024-09-18 14:17:40 +02:00
theexiile1305
6fda661dc7
added content type, mime type and content disposition for S/MIME singing purpose 2024-09-18 14:16:48 +02:00
theexiile1305
158c1b0458
moved S/MIME initialization into msg and the pointer to the data structure, also adjusted tests 2024-09-18 14:13:24 +02:00
theexiile1305
07d9654ce7
Attribute sMimeSinging was added in msg that indicates whether the message should be singed with S/MIME when it's sent. Also, sMimeAuthConfig was introduced in client so that required privateKey and certificate can be used for S/MIME signing. 2024-09-15 19:56:15 +02:00
24 changed files with 1674 additions and 18 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 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.

View 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-----

View 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
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

View 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-----

View 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
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 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)
}

View file

@ -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())
}
})
}
}

2
go.mod
View file

@ -9,4 +9,4 @@ go 1.16
require ( require (
golang.org/x/crypto v0.28.0 golang.org/x/crypto v0.28.0
golang.org/x/text v0.19.0 golang.org/x/text v0.19.0
) )

2
go.sum
View file

@ -63,4 +63,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.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 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/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=

107
msg.go
View file

@ -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,50 @@ 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)
}
switch keyPairTlS.PrivateKey.(type) {
case *rsa.PrivateKey:
if intermediateCertificate == nil {
return m.SignWithSMimeRSA(keyPairTlS.PrivateKey.(*rsa.PrivateKey), keyPairTlS.Leaf, nil)
}
return m.SignWithSMimeRSA(keyPairTlS.PrivateKey.(*rsa.PrivateKey), keyPairTlS.Leaf, intermediateCertificate)
case *ecdsa.PrivateKey:
if intermediateCertificate == nil {
return m.SignWithSMimeECDSA(keyPairTlS.PrivateKey.(*ecdsa.PrivateKey), keyPairTlS.Leaf, nil)
}
return m.SignWithSMimeECDSA(keyPairTlS.PrivateKey.(*ecdsa.PrivateKey), keyPairTlS.Leaf, intermediateCertificate)
default:
return fmt.Errorf("unsupported private key type: %T", keyPairTlS.PrivateKey)
}
}
// 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
@ -2115,6 +2164,38 @@ 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 := 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. // 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
@ -2132,7 +2213,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
} }
@ -2496,7 +2588,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.
@ -2514,6 +2606,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

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 := 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")
}
}
// TestMsg_hasRelated tests the hasRelated() method of the Msg // TestMsg_hasRelated tests the hasRelated() method of the Msg
func TestMsg_hasRelated(t *testing.T) { func TestMsg_hasRelated(t *testing.T) {
m := NewMsg() 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 := 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")
}
}
// TestMsg_WriteTo_fails tests the WriteTo() method of the Msg but with a failing body writer function // TestMsg_WriteTo_fails tests the WriteTo() method of the Msg but with a failing body writer function
func TestMsg_WriteTo_fails(t *testing.T) { func TestMsg_WriteTo_fails(t *testing.T) {
m := NewMsg() m := NewMsg()
@ -3246,6 +3343,66 @@ func TestNewMsgWithNoDefaultUserAgent(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")
}
}
// 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)
}
}
// Fuzzing tests // Fuzzing tests
func FuzzMsg_Subject(f *testing.F) { func FuzzMsg_Subject(f *testing.F) {
f.Add("Testsubject") f.Add("Testsubject")
@ -3273,3 +3430,84 @@ func FuzzMsg_From(f *testing.F) {
m.Reset() m.Reset()
}) })
} }
// 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())
}
}

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() { 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)
} }

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") 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 := 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
View file

@ -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
}
}

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 // 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
View 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
View 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
}

192
smime.go Normal file
View file

@ -0,0 +1,192 @@
// 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"
"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 {
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
}

244
smime_test.go Normal file
View file

@ -0,0 +1,244 @@
// 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 := 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)
})
}

64
util_test.go Normal file
View file

@ -0,0 +1,64 @@
// 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) and the associated private key (ECDSA) form 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) and the associated private key (ECDSA) form 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
}