mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-23 06:10:48 +01:00
Compare commits
5 commits
1e3f4fe757
...
bf9d55cdd1
Author | SHA1 | Date | |
---|---|---|---|
|
bf9d55cdd1 | ||
|
4b60557518 | ||
|
9bdb741e05 | ||
|
2368113872 | ||
|
12edb2724b |
9 changed files with 411 additions and 103 deletions
62
dummy-chain-cert.pem
Normal file
62
dummy-chain-cert.pem
Normal file
|
@ -0,0 +1,62 @@
|
|||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
-----END CERTIFICATE-----
|
||||
-----BEGIN CERTIFICATE-----
|
||||
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
|
||||
-----END CERTIFICATE-----
|
52
dummy-child-key.pem
Normal file
52
dummy-child-key.pem
Normal file
|
@ -0,0 +1,52 @@
|
|||
-----BEGIN PRIVATE KEY-----
|
||||
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
|
||||
S++xGnGHEUSEl7cWnO8k0RyJzzc=
|
||||
-----END PRIVATE KEY-----
|
44
msg.go
44
msg.go
|
@ -7,8 +7,7 @@ package mail
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/tls"
|
||||
"embed"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
@ -208,8 +207,8 @@ func WithNoDefaultUserAgent() MsgOption {
|
|||
}
|
||||
|
||||
// SignWithSMime configures the Msg to be signed with S/MIME
|
||||
func (m *Msg) SignWithSMime(privateKey *rsa.PrivateKey, certificate *x509.Certificate) error {
|
||||
sMime, err := NewSMime(privateKey, certificate)
|
||||
func (m *Msg) SignWithSMime(keyPair *tls.Certificate) error {
|
||||
sMime, err := newSMime(keyPair)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -996,28 +995,31 @@ func (m *Msg) applyMiddlewares(msg *Msg) *Msg {
|
|||
|
||||
// signMessage sign the Msg with S/MIME
|
||||
func (m *Msg) signMessage(msg *Msg) (*Msg, error) {
|
||||
currentPart := m.GetParts()[0]
|
||||
currentPart.SetEncoding(EncodingUSASCII)
|
||||
currentPart.SetContentType(TypeTextPlain)
|
||||
content, err := currentPart.GetContent()
|
||||
signedPart := msg.GetParts()[0]
|
||||
body, err := signedPart.GetContent()
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to extract content from part")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signedContent, err := m.sMime.Sign(content)
|
||||
signaturePart, err := m.createSignaturePart(signedPart.GetEncoding(), signedPart.GetContentType(), signedPart.GetCharset(), body)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to sign message")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
signedPart := msg.newPart(
|
||||
typeSMimeSigned,
|
||||
WithPartEncoding(EncodingB64),
|
||||
WithContentDisposition(DispositionSMime),
|
||||
)
|
||||
signedPart.SetContent(*signedContent)
|
||||
msg.parts = append(msg.parts, signedPart)
|
||||
m.parts = append(m.parts, signaturePart)
|
||||
|
||||
return msg, nil
|
||||
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
|
||||
|
@ -1025,7 +1027,7 @@ func (m *Msg) WriteTo(writer io.Writer) (int64, error) {
|
|||
mw := &msgWriter{writer: writer, charset: m.charset, encoder: m.encoder}
|
||||
msg := m.applyMiddlewares(m)
|
||||
|
||||
if m.sMime != nil {
|
||||
if m.hasSMime() {
|
||||
signedMsg, err := m.signMessage(msg)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
|
@ -1221,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
|
||||
|
|
131
msg_test.go
131
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, "<b>HTML</b>")
|
||||
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,32 +3263,33 @@ func TestNewMsgWithNoDefaultUserAgent(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestWithSMimeSinging_ValidPrivateKey tests WithSMimeSinging with given privateKey
|
||||
func TestWithSMimeSinging_ValidPrivateKey(t *testing.T) {
|
||||
privateKey, err := getDummyPrivateKey()
|
||||
// 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 private key: %s", err)
|
||||
t.Errorf("failed to load dummy certificate. Cause: %v", err)
|
||||
}
|
||||
certificate, err := getDummyCertificate(privateKey)
|
||||
if err != nil {
|
||||
t.Errorf("failed to load dummy certificate: %s", err)
|
||||
}
|
||||
|
||||
m := NewMsg()
|
||||
if err := m.SignWithSMime(privateKey, certificate); err != nil {
|
||||
if err := m.SignWithSMime(keyPair); err != nil {
|
||||
t.Errorf("failed to set sMime. Cause: %v", err)
|
||||
}
|
||||
if m.sMime.privateKey != privateKey {
|
||||
t.Errorf("WithSMimeSinging. Expected %v, got: %v", privateKey, m.sMime.privateKey)
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
// TestWithSMimeSinging_InvalidPrivateKey tests WithSMimeSinging with given invalid privateKey
|
||||
func TestWithSMimeSinging_InvalidPrivateKey(t *testing.T) {
|
||||
// TestSignWithSMime_InvalidKeyPair tests WithSMimeSinging with given invalid key pair
|
||||
func TestSignWithSMime_InvalidKeyPair(t *testing.T) {
|
||||
m := NewMsg()
|
||||
|
||||
err := m.SignWithSMime(nil, nil)
|
||||
if !errors.Is(err, ErrInvalidPrivateKey) {
|
||||
err := m.SignWithSMime(nil)
|
||||
if !errors.Is(err, ErrInvalidKeyPair) {
|
||||
t.Errorf("failed to check sMimeAuthConfig values correctly: %s", err)
|
||||
}
|
||||
}
|
||||
|
@ -3303,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())
|
||||
}
|
||||
}
|
||||
|
|
17
msgwriter.go
17
msgwriter.go
|
@ -100,7 +100,7 @@ func (mw *msgWriter) writeMsg(msg *Msg) {
|
|||
mw.startMP(MIMERelated, msg.boundary)
|
||||
mw.writeString(DoubleNewLine)
|
||||
}
|
||||
if msg.hasAlt() && !msg.hasSMime() {
|
||||
if msg.hasAlt() {
|
||||
mw.startMP(MIMEAlternative, msg.boundary)
|
||||
mw.writeString(DoubleNewLine)
|
||||
}
|
||||
|
@ -241,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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -273,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
|
||||
|
@ -322,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
|
||||
|
@ -337,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)
|
||||
|
@ -355,7 +354,7 @@ func (mw *msgWriter) writeBody(writeFunc func(io.Writer) (int64, error), encodin
|
|||
mw.bytesWritten += n
|
||||
}
|
||||
return
|
||||
default:
|
||||
} else {
|
||||
encodedWriter = quotedprintable.NewWriter(writer)
|
||||
}
|
||||
|
||||
|
|
|
@ -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" <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, `multipart/signed; protocol="application/pkcs7-signature"; micalg=sha256;`) {
|
||||
t.Errorf("writeMsg failed. Expected PGP encoding header but didn't find it in message output")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -275,6 +275,7 @@ func TestPart_IsSMimeSigned(t *testing.T) {
|
|||
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)
|
||||
|
|
137
sime.go
137
sime.go
|
@ -1,18 +1,20 @@
|
|||
package mail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go.mozilla.org/pkcs7"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidPrivateKey should be used if private key is invalid
|
||||
ErrInvalidPrivateKey = errors.New("invalid private key")
|
||||
|
||||
// ErrInvalidCertificate should be used if certificate is invalid
|
||||
ErrInvalidCertificate = errors.New("invalid certificate")
|
||||
// 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")
|
||||
|
@ -22,49 +24,136 @@ var (
|
|||
|
||||
// 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 *rsa.PrivateKey
|
||||
func NewSMime(privateKey *rsa.PrivateKey, certificate *x509.Certificate) (*SMime, error) {
|
||||
if privateKey == nil {
|
||||
return nil, ErrInvalidPrivateKey
|
||||
// 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
|
||||
}
|
||||
|
||||
if certificate == nil {
|
||||
return nil, ErrInvalidCertificate
|
||||
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: privateKey,
|
||||
certificate: certificate,
|
||||
privateKey: keyPair.PrivateKey.(*rsa.PrivateKey),
|
||||
certificate: keyPair.Leaf,
|
||||
parentCertificates: parentCertificates,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Sign the content with the given privateKey of the method NewSMime
|
||||
func (sm *SMime) Sign(content []byte) (*string, error) {
|
||||
toBeSigned, err := pkcs7.NewSignedData(content)
|
||||
// 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"))
|
||||
|
||||
toBeSigned.SetDigestAlgorithm(pkcs7.OIDDigestAlgorithmSHA256)
|
||||
tmp, err := pkcs7.NewSignedData(toBeSigned)
|
||||
tmp.SetDigestAlgorithm(pkcs7.OIDDigestAlgorithmSHA256)
|
||||
if err != nil {
|
||||
return nil, ErrCouldNotInitialize
|
||||
return ErrCouldNotInitialize
|
||||
}
|
||||
|
||||
if err = toBeSigned.AddSigner(sm.certificate, sm.privateKey, pkcs7.SignerInfoConfig{}); err != nil {
|
||||
return nil, ErrCouldNotAddSigner
|
||||
if err = tmp.AddSignerChain(sm.certificate, sm.privateKey, sm.parentCertificates, pkcs7.SignerInfoConfig{}); err != nil {
|
||||
return ErrCouldNotAddSigner
|
||||
}
|
||||
|
||||
signed, err := toBeSigned.Finish()
|
||||
signatureDER, err := tmp.Finish()
|
||||
if err != nil {
|
||||
return nil, ErrCouldNotFinishSigning
|
||||
return ErrCouldNotFinishSigning
|
||||
}
|
||||
|
||||
signedData := string(signed)
|
||||
pemMsg, err := encodeToPEM(signatureDER)
|
||||
if err != nil {
|
||||
return ErrCouldNoEncodeToPEM
|
||||
}
|
||||
signaturePart.SetContent(*pemMsg)
|
||||
|
||||
return &signedData, nil
|
||||
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
|
||||
}
|
||||
|
|
43
util_test.go
43
util_test.go
|
@ -1,41 +1,22 @@
|
|||
package mail
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"math/big"
|
||||
"time"
|
||||
)
|
||||
|
||||
func getDummyPrivateKey() (*rsa.PrivateKey, error) {
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
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
|
||||
}
|
||||
return privateKey, nil
|
||||
}
|
||||
|
||||
func getDummyCertificate(privateKey *rsa.PrivateKey) (*x509.Certificate, error) {
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1234),
|
||||
Subject: pkix.Name{Organization: []string{"My Organization"}},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().AddDate(1, 0, 0),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
}
|
||||
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, template, template, &privateKey.PublicKey, privateKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cert, err := x509.ParseCertificate(certDER)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cert, nil
|
||||
|
||||
keyPair.Leaf, err = x509.ParseCertificate(keyPair.Certificate[0])
|
||||
|
||||
return &keyPair, nil
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue