diff --git a/dummy-chain-cert.pem b/dummy-chain-cert.pem
new file mode 100644
index 0000000..4daceaa
--- /dev/null
+++ b/dummy-chain-cert.pem
@@ -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-----
diff --git a/dummy-child-key.pem b/dummy-child-key.pem
new file mode 100644
index 0000000..2a8639b
--- /dev/null
+++ b/dummy-child-key.pem
@@ -0,0 +1,52 @@
+-----BEGIN PRIVATE KEY-----
+MIIJQAIBADANBgkqhkiG9w0BAQEFAASCCSowggkmAgEAAoICAQCiLh0JJTRRBhmU
+yiMKALHtTOK7T20Bwy+fG0SO6RlBc+hSuuX/n6znXcNgOBlQ2Gg3+p1on/bmcKnG
+N/SCiVBLpROiwxg3blQbZ7B7Jors/MopGk0LIBOXHPtAuYbF4J6ND5Ol6sgeGjMn
+omwRjZlfeuBlHY345MqvcwH/lPhOlKme+tWD+bsFh08NGS+3NdQGP6dA2bRVrPXh
+LXStHEmqfKO9EMVLWv+77tYhZESD6XgnA8pWjbdr9jajCsrQWrCG3jqHtzHNxtwf
+7xfRwwgoUhLEvue6SBVokZGVmDhvWdRt2sjtLcWWJCI3p7M+NXRt5qf6iu24wLBd
+zIDuWfgooWu5vBzNZjSTh2if6R1as9BdwASwy1n2HMvqpzgA+f/rXDFbvVc7WIKi
+uGzfWApBrL0qTCQBuSyepH0G4rQ2sJtI5U8QOKBO76nQJq5WDQgefBX4GDI8aJ6q
+X+teQq1AqERUmLWx4WlTwxSo5X+d1jY9I8f61CRKVfIRgMvAZUhm2h6RnoVIgq7G
+7W3HdC3RT/f758njI7oIv5bhiyqpgyKr3cYhmn0enjP+YtjY85m51q005qzRLfaT
+YiwMR4qyJW4ZOEPntPs20CD+e+Pi2JLONpRdcsSrkqZusVjm5PFy2e5RyNFXTupU
+H2KVrgTRHL3GG2KWF5PmBdkhQfGG1QIDAQABAoICAAL3IruL6/zP5DPZ9RkiL1m/
+lHPhP7/sWKTfWMPf4ChX6XAHWeYraqNPvx8/bESdstLAvx3piyvcapRupN9DsVTu
+SrKytjXft6OEVWFLEveHk3F8B+9ewbMY4BmsQOjpVR9J7+6SZmpUB2MdD3lpdY07
+YSuamff0dcNdV2+NEZth6bit++iJFc9rTI/OwBZLMTVsp+oVpKh7w72h1DfboaF/
+oU9tYHWFWTRUfSzoqm7Q4POKmII4BhA+1QWIUIX1OLQocepzgfw72Tj+GlRQ7WAu
+IToIBqbRxfsNflaWDSv15UrE7OCDLUdlXILOd0GgtJaYTsX7X7ZMU1mIoqX0IBsk
+KRCep+7BTA8VYXOlW26tPEcsj2Vp7tdghptTaEtdx05delaYaX18rxdprNz+VV0Z
+jNVIgShJC4vMEVQtOSOyznavF9OONScBG4e0E9rSHbYudtvrp8WOoaDK6mzAwo6w
+wShYwwzFmf0Y2FbsENvNiNw5cqKT4WQoCXM6WS9BjPfJqz4M7V8pYa3pCE4M+oXP
+sfQDgkpyXg88Dez1N78cbpr4GKsI9odFmphivQngXF1H3N8UKLCjW/EQNKHoydTn
+nTtIfSY1G5hlDS0nCqTn6LvI2W887Da+ASWGtgGn0opLUW5Kg5fPurFjoCEP4mNg
+JQ8nX9q2N5AogHld0h1ZAoIBAQDVRSty2Q7+oQh0qWGI0eDESeAHd/OKnwELAsMx
+pfXABhB/CjO0/Iy4MgBL1dMb1S64gHhHi816CfxoSQuaUNjOcAWxhLTbnPEx8eaS
+SFvdnd3itEC0T6Cg1r8mklHXrz6WH8vtLfu3pc8svGp+xJ5Rmlg1G3rhYFce6csg
+lgeP4n4vjxOZtds3Jr8oHiW0/KYrLUK8/TgFOwYvWZQ8Lk92Oz+Fcd+RkegSkzIB
+hHpqIMQz4T5uirjmxKX9573X17cVO3LuCBSite1uiyXRAIrGEOOyHwv3xC2h/bV1
+6IYSiuwJOxDxZAmYuWYBtlvfWrOypEuWogZQv8C/zxDpkDSLAoIBAQDCrHxmh5NI
+ksWQg0W6176uktwyypFBh8REE1VfCgoxJ84TVOQzEBiNQpRj/FUCe4e7ZElIAks+
+N9mh8smJLvHDVo7033NIfAZCfCbLve8uWSGAm5x+aKS7kG5gynojKT8vl0y88m5o
+Nm+BfvpQKjb6n6jibZZRsQsyz5KBct+Gb9gyA81jjrFudBmYy9WZeEm6pjzQtkuq
+I0xCbQFwB+It/utjz7okuAWk5fPU0LRqOvvEMJjf2DyvIK45FDsBF3zGlG8Zunnh
+q3o28zXdQCvYxY5Ik8zRuSTTAQnaJ3/zUvqR2g3PoSF8d7/WSkzNK+ZhTANqPkq1
+SOg515Na9L4fAoH/Vc5+rLaoUcp4nHeJxoKq7E7M1DRuyFcxFD0IS/F57siB2ptA
+MpFqDLIRbHGbfpdHNPR7cE3PXkqmQ08gW/YrROPNZp7+JV3/rRimrDRwwbnCjHP5
+lJJ1DkFYpyw3wY/AnqYsZkEaBcmwkU89icOR70MqOjPUPNmGM+nc0D+My1dVbc0j
+FbUVfhsYzgtTIH6GXNjZATDgWTpmQqbH/W6kie1MoWQvj2Ik/VQ7ymCC4DBOwJDf
+jZpCypZUMtQKjc083E4O77ZQlyabYN6bWHvfWdFxyziyl/1WXta1K7tiNhOu5Aff
+yT92nPv7DrVQQY08v6NaxkBqShLcek/VfiOHAoIBAAtzd/HUAb7gG0zv29csv6On
+Mdqu/bJcGRhkBr6LaaQQkleiw7WZOch9ZRsoiZuWxpooQQNCV0i2ok+bZ21xXHlA
+CzKuPirCWN/qS6HqbzpLtePJw3/QCfiae1OoNV0CHRxgivwGSqZIpXB5lqHGiete
+HuIKzi/J+T2o5hZFOo6+33m5rYgwqZE0tRi+zLa1U6juBF/GiVbdsqupm88KN6y6
+9P+vBWUJihN0D06yZBpnk82riiKIprEqe/URkpLy3b0UmCBsTqUOoCbBUabNEocy
+v7bXMtIXUOo0gm7ZqfYXKHQR3oQbF0wqAxfI0RG0hl2syfqi5WQagMZ+PsW35cMC
+ggEBAM6kIM/zUnNBFla+oTiPCyoII7nGmGz8dT9IhH0+6T5nNzG4O6D28A3MJcs1
+C3pfukCCeWOnDNCIQ7C9Hx5XcCDoI4eD6zCRy+7zXxn0FxYS+O6lczB2mXGE69Yp
+n3qb7P4XqZYex3dT72czJUlY6nFB2e0FmyvSoez8fsH9Ws78c4JGO953klam/emA
+Hc8nB3CyM8rb2JlM3WeQbmo+Sbi0Yvj+MWM2AnTXx0xyaFXKP4WGD8hAxTvf9tSX
+3NAPVBXku4zRpvoXyyrcBVd9vwE0qBr3eWCuci8aDD6RAAJgb8HHX9RjRF/phFpY
+S++xGnGHEUSEl7cWnO8k0RyJzzc=
+-----END PRIVATE KEY-----
diff --git a/encoding.go b/encoding.go
index 47213da..77243fc 100644
--- a/encoding.go
+++ b/encoding.go
@@ -149,6 +149,7 @@ const (
TypePGPEncrypted ContentType = "application/pgp-encrypted"
TypeTextHTML ContentType = "text/html"
TypeTextPlain ContentType = "text/plain"
+ typeSMimeSigned ContentType = `application/pkcs7-signature; name="smime.p7s"`
)
// List of MIMETypes
@@ -156,6 +157,7 @@ const (
MIMEAlternative MIMEType = "alternative"
MIMEMixed MIMEType = "mixed"
MIMERelated MIMEType = "related"
+ MIMESMime MIMEType = `signed; protocol="application/pkcs7-signature"; micalg=sha256`
)
// String is a standard method to convert an Charset into a printable format
diff --git a/encoding_test.go b/encoding_test.go
index 14711b7..11bf991 100644
--- a/encoding_test.go
+++ b/encoding_test.go
@@ -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) {
diff --git a/go.mod b/go.mod
index b155b9b..a59f78a 100644
--- a/go.mod
+++ b/go.mod
@@ -5,3 +5,5 @@
module github.com/wneessen/go-mail
go 1.16
+
+require go.mozilla.org/pkcs7 v0.9.0
diff --git a/go.sum b/go.sum
index e69de29..4ca3691 100644
--- a/go.sum
+++ b/go.sum
@@ -0,0 +1,2 @@
+go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI=
+go.mozilla.org/pkcs7 v0.9.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
diff --git a/msg.go b/msg.go
index b66bffe..1dbceca 100644
--- a/msg.go
+++ b/msg.go
@@ -7,6 +7,7 @@ package mail
import (
"bytes"
"context"
+ "crypto/tls"
"embed"
"errors"
"fmt"
@@ -120,6 +121,9 @@ type Msg struct {
// noDefaultUserAgent indicates whether the default User Agent will be excluded for the Msg when it's sent.
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
@@ -202,6 +206,16 @@ func WithNoDefaultUserAgent() MsgOption {
}
}
+// SignWithSMime configures the Msg to be signed with S/MIME
+func (m *Msg) SignWithSMime(keyPair *tls.Certificate) error {
+ sMime, err := newSMime(keyPair)
+ if err != nil {
+ return err
+ }
+ m.sMime = sMime
+ return nil
+}
+
// SetCharset sets the encoding charset of the Msg
func (m *Msg) SetCharset(c Charset) {
m.charset = c
@@ -979,10 +993,50 @@ 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
+}
+
+func (m *Msg) createSignaturePart(encoding Encoding, contentType ContentType, charSet Charset, body []byte) (*Part, error) {
+ message := m.sMime.createMessage(encoding, contentType, charSet, body)
+ signaturePart := m.newPart(typeSMimeSigned, WithPartEncoding(EncodingB64), WithSMimeSinging())
+
+ if err := m.sMime.sign(signaturePart, message); err != nil {
+ return nil, err
+ }
+
+ return signaturePart, nil
+}
+
// WriteTo writes the formated Msg into a give io.Writer and satisfies the io.WriteTo interface
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
}
@@ -1169,7 +1223,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
@@ -1177,6 +1231,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
func (m *Msg) hasRelated() bool {
return m.pgptype == 0 && ((len(m.parts) > 0 && len(m.embeds) > 0) || len(m.embeds) > 1)
diff --git a/msg_test.go b/msg_test.go
index f570b02..881ff55 100644
--- a/msg_test.go
+++ b/msg_test.go
@@ -1907,6 +1907,23 @@ 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) {
+ keyPair, err := getDummyCertificate()
+ if err != nil {
+ t.Errorf("failed to load dummy certificate. Cause: %v", err)
+ }
+ m := NewMsg()
+ m.SetBodyString(TypeTextPlain, "Plain")
+ m.AddAlternativeString(TypeTextHTML, "HTML")
+ if err := m.SignWithSMime(keyPair); err != nil {
+ t.Errorf("set of certificate was not successful")
+ }
+ if m.hasAlt() {
+ t.Errorf("mail has alternative parts and S/MIME is active, but hasAlt() returned true")
+ }
+}
+
// TestMsg_hasRelated tests the hasRelated() method of the Msg
func TestMsg_hasRelated(t *testing.T) {
m := NewMsg()
@@ -3246,6 +3263,37 @@ func TestNewMsgWithNoDefaultUserAgent(t *testing.T) {
}
}
+// TestSignWithSMime_ValidKeyPair tests WithSMimeSinging with given key pair
+func TestSignWithSMime_ValidKeyPair(t *testing.T) {
+ keyPair, err := getDummyCertificate()
+ if err != nil {
+ t.Errorf("failed to load dummy certificate. Cause: %v", err)
+ }
+ m := NewMsg()
+ if err := m.SignWithSMime(keyPair); 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")
+ }
+ if len(m.sMime.parentCertificates) != len(keyPair.Certificate[:1]) {
+ t.Errorf("WithSMimeSinging() - no certificate is given")
+ }
+}
+
+// TestSignWithSMime_InvalidKeyPair tests WithSMimeSinging with given invalid key pair
+func TestSignWithSMime_InvalidKeyPair(t *testing.T) {
+ m := NewMsg()
+
+ err := m.SignWithSMime(nil)
+ if !errors.Is(err, ErrInvalidKeyPair) {
+ t.Errorf("failed to check sMimeAuthConfig values correctly: %s", err)
+ }
+}
+
// Fuzzing tests
func FuzzMsg_Subject(f *testing.F) {
f.Add("Testsubject")
@@ -3273,3 +3321,84 @@ func FuzzMsg_From(f *testing.F) {
m.Reset()
})
}
+
+// TestMsg_createSignaturePart tests the Msg.createSignaturePart method
+func TestMsg_createSignaturePart(t *testing.T) {
+ keyPair, err := getDummyCertificate()
+ if err != nil {
+ t.Errorf("failed to load dummy certificate. Cause: %v", err)
+ }
+ m := NewMsg()
+ if err := m.SignWithSMime(keyPair); err != nil {
+ t.Errorf("set of certificate was not successful")
+ }
+ 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) {
+ keyPair, err := getDummyCertificate()
+ if err != nil {
+ t.Errorf("failed to load dummy certificate. Cause: %v", err)
+ }
+ body := []byte("This is the body")
+ m := NewMsg()
+ m.SetBodyString(TypeTextPlain, string(body))
+ if err := m.SignWithSMime(keyPair); err != nil {
+ t.Errorf("set of certificate was not successful")
+ }
+ 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())
+ }
+}
diff --git a/msgwriter.go b/msgwriter.go
index ff1b47b..7c1e88e 100644
--- a/msgwriter.go
+++ b/msgwriter.go
@@ -88,6 +88,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)
@@ -237,7 +241,7 @@ func (mw *msgWriter) addFiles(files []*File, isAttachment bool) {
}
if mw.err == nil {
- mw.writeBody(file.Writer, encoding)
+ mw.writeBody(file.Writer, encoding, false)
}
}
}
@@ -269,7 +273,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
@@ -318,7 +322,7 @@ func (mw *msgWriter) writeHeader(key Header, values ...string) {
}
// writeBody writes an io.Reader into an io.Writer using provided Encoding
-func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encoding Encoding) {
+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
@@ -333,12 +337,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 {
_, err = writeFunc(&writeBuffer)
if err != nil {
mw.err = fmt.Errorf("bodyWriter function: %w", err)
@@ -351,7 +354,7 @@ func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encodin
mw.bytesWritten += n
}
return
- default:
+ } else {
encodedWriter = quotedprintable.NewWriter(writer)
}
diff --git a/msgwriter_test.go b/msgwriter_test.go
index a41e5d3..ca1a982 100644
--- a/msgwriter_test.go
+++ b/msgwriter_test.go
@@ -154,3 +154,26 @@ 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) {
+ keyPair, err := getDummyCertificate()
+ if err != nil {
+ t.Errorf("failed to load dummy certificate. Cause: %v", err)
+ }
+ m := NewMsg()
+ if err := m.SignWithSMime(keyPair); err != nil {
+ t.Errorf("set of certificate was not successful")
+ }
+ _ = m.From(`"Toni Tester" `)
+ _ = m.To(`"Toni Receiver" `)
+ 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, `multipart/signed; protocol="application/pkcs7-signature"; micalg=sha256;`) {
+ t.Errorf("writeMsg failed. Expected PGP encoding header but didn't find it in message output")
+ }
+}
diff --git a/part.go b/part.go
index 7c76b7d..035b342 100644
--- a/part.go
+++ b/part.go
@@ -20,6 +20,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 byte slice
@@ -56,6 +57,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
func (p *Part) SetContent(content string) {
buffer := bytes.NewBufferString(content)
@@ -82,6 +88,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
func (p *Part) SetWriteFunc(writeFunc func(io.Writer) (int64, error)) {
p.writeFunc = writeFunc
@@ -113,3 +124,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
+ }
+}
diff --git a/part_test.go b/part_test.go
index a196986..c1a8ac7 100644
--- a/part_test.go
+++ b/part_test.go
@@ -102,6 +102,24 @@ func TestPart_WithPartContentDescription(t *testing.T) {
}
}
+// TestPart_WithSMimeSinging tests the WithSMimeSinging method
+func TestPart_WithSMimeSinging(t *testing.T) {
+ m := NewMsg()
+ part := m.newPart(TypeTextPlain, WithSMimeSinging())
+ if part == nil {
+ t.Errorf("newPart() WithSMimeSinging() failed: no part returned")
+ return
+ }
+ if part.smime != true {
+ t.Errorf("newPart() WithSMimeSinging() failed: expected: %v, got: %v", true, part.smime)
+ }
+ part.smime = true
+ part.SetIsSMimeSigned(false)
+ if part.smime != false {
+ t.Errorf("newPart() SetIsSMimeSigned() failed: expected: %v, got: %v", false, part.smime)
+ }
+}
+
// TestPartContentType tests Part.SetContentType
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ß"
diff --git a/sime.go b/sime.go
new file mode 100644
index 0000000..fe05158
--- /dev/null
+++ b/sime.go
@@ -0,0 +1,159 @@
+package mail
+
+import (
+ "bytes"
+ "crypto/rsa"
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/pem"
+ "errors"
+ "fmt"
+ "go.mozilla.org/pkcs7"
+ "strings"
+)
+
+var (
+ // ErrInvalidKeyPair should be used if key pair is invalid
+ ErrInvalidKeyPair = errors.New("invalid key pair")
+
+ // 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
+ parentCertificates []*x509.Certificate
+}
+
+// NewSMime construct a new instance of SMime with a provided *tls.Certificate
+func newSMime(keyPair *tls.Certificate) (*SMime, error) {
+ if keyPair == nil {
+ return nil, ErrInvalidKeyPair
+ }
+
+ parentCertificates := make([]*x509.Certificate, 0)
+ for _, cert := range keyPair.Certificate[1:] {
+ c, err := x509.ParseCertificate(cert)
+ if err != nil {
+ return nil, err
+ }
+ parentCertificates = append(parentCertificates, c)
+ }
+
+ return &SMime{
+ privateKey: keyPair.PrivateKey.(*rsa.PrivateKey),
+ certificate: keyPair.Leaf,
+ parentCertificates: parentCertificates,
+ }, nil
+}
+
+// sign with the S/MIME method the message of the actual *Part
+func (sm *SMime) sign(signaturePart *Part, message string) error {
+ lines := parseLines([]byte(message))
+ toBeSigned := lines.bytesFromLines([]byte("\r\n"))
+
+ tmp, err := pkcs7.NewSignedData(toBeSigned)
+ tmp.SetDigestAlgorithm(pkcs7.OIDDigestAlgorithmSHA256)
+ if err != nil {
+ return ErrCouldNotInitialize
+ }
+
+ if err = tmp.AddSignerChain(sm.certificate, sm.privateKey, sm.parentCertificates, pkcs7.SignerInfoConfig{}); err != nil {
+ return ErrCouldNotAddSigner
+ }
+
+ signatureDER, err := tmp.Finish()
+ if err != nil {
+ return ErrCouldNotFinishSigning
+ }
+
+ pemMsg, err := encodeToPEM(signatureDER)
+ if err != nil {
+ return ErrCouldNoEncodeToPEM
+ }
+ signaturePart.SetContent(*pemMsg)
+
+ return nil
+}
+
+// createMessage prepares the message that will be used for the sign method later
+func (sm *SMime) createMessage(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.ReplaceAll(r, "-----BEGIN -----\n", "")
+ r = strings.ReplaceAll(r, "-----END -----\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
+}
diff --git a/util_test.go b/util_test.go
new file mode 100644
index 0000000..47bac43
--- /dev/null
+++ b/util_test.go
@@ -0,0 +1,22 @@
+package mail
+
+import (
+ "crypto/tls"
+ "crypto/x509"
+)
+
+const (
+ certFilePath = "dummy-chain-cert.pem"
+ keyFilePath = "dummy-child-key.pem"
+)
+
+func getDummyCertificate() (*tls.Certificate, error) {
+ keyPair, err := tls.LoadX509KeyPair(certFilePath, keyFilePath)
+ if err != nil {
+ return nil, err
+ }
+
+ keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0])
+
+ return &keyPair, nil
+}