mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-23 06:10:48 +01:00
Compare commits
16 commits
c01e7964c3
...
1e3f4fe757
Author | SHA1 | Date | |
---|---|---|---|
|
1e3f4fe757 | ||
|
3154649420 | ||
|
65b9fd07da | ||
077c85bea0 | |||
909e699b99 | |||
|
b97073db19 | ||
|
d75d990124 | ||
0676d99f20 | |||
|
d6725b2d63 | ||
|
894936092e | ||
|
41a81966d8 | ||
|
94942ed383 | ||
|
e0a59dba6d | ||
|
6fda661dc7 | ||
|
158c1b0458 | ||
|
07d9654ce7 |
14 changed files with 282 additions and 7 deletions
6
.github/workflows/codeql-analysis.yml
vendored
6
.github/workflows/codeql-analysis.yml
vendored
|
@ -54,7 +54,7 @@ jobs:
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8
|
uses: github/codeql-action/init@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
|
@ -65,7 +65,7 @@ jobs:
|
||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8
|
uses: github/codeql-action/autobuild@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 https://git.io/JvXDl
|
# 📚 https://git.io/JvXDl
|
||||||
|
@ -79,4 +79,4 @@ jobs:
|
||||||
# make release
|
# make release
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8
|
uses: github/codeql-action/analyze@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9
|
||||||
|
|
2
.github/workflows/scorecards.yml
vendored
2
.github/workflows/scorecards.yml
vendored
|
@ -75,6 +75,6 @@ jobs:
|
||||||
|
|
||||||
# Upload the results to GitHub's code scanning dashboard.
|
# Upload the results to GitHub's code scanning dashboard.
|
||||||
- name: "Upload to code-scanning"
|
- name: "Upload to code-scanning"
|
||||||
uses: github/codeql-action/upload-sarif@294a9d92911152fe08befb9ec03e240add280cb3 # v3.26.8
|
uses: github/codeql-action/upload-sarif@461ef6c76dfe95d5c364de2f431ddbd31a417628 # v3.26.9
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
|
2
.github/workflows/sonarqube.yml
vendored
2
.github/workflows/sonarqube.yml
vendored
|
@ -44,7 +44,7 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
go test -v -race --coverprofile=./cov.out ./...
|
go test -v -race --coverprofile=./cov.out ./...
|
||||||
|
|
||||||
- uses: sonarsource/sonarqube-scan-action@0c0f3958d90fc466625f1d1af1f47bddd4cc6bd1 # master
|
- uses: sonarsource/sonarqube-scan-action@884b79409bbd464b2a59edc326a4b77dc56b2195 # master
|
||||||
env:
|
env:
|
||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
||||||
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
|
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}
|
||||||
|
|
|
@ -149,6 +149,7 @@ const (
|
||||||
TypePGPEncrypted ContentType = "application/pgp-encrypted"
|
TypePGPEncrypted ContentType = "application/pgp-encrypted"
|
||||||
TypeTextHTML ContentType = "text/html"
|
TypeTextHTML ContentType = "text/html"
|
||||||
TypeTextPlain ContentType = "text/plain"
|
TypeTextPlain ContentType = "text/plain"
|
||||||
|
typeSMimeSigned ContentType = `application/pkcs7-signature; name="smime.p7s"`
|
||||||
)
|
)
|
||||||
|
|
||||||
// List of MIMETypes
|
// List of MIMETypes
|
||||||
|
@ -156,6 +157,7 @@ const (
|
||||||
MIMEAlternative MIMEType = "alternative"
|
MIMEAlternative MIMEType = "alternative"
|
||||||
MIMEMixed MIMEType = "mixed"
|
MIMEMixed MIMEType = "mixed"
|
||||||
MIMERelated MIMEType = "related"
|
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
|
// String is a standard method to convert an Charset into a printable format
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -5,3 +5,5 @@
|
||||||
module github.com/wneessen/go-mail
|
module github.com/wneessen/go-mail
|
||||||
|
|
||||||
go 1.16
|
go 1.16
|
||||||
|
|
||||||
|
require go.mozilla.org/pkcs7 v0.9.0
|
||||||
|
|
2
go.sum
2
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=
|
59
msg.go
59
msg.go
|
@ -7,6 +7,8 @@ package mail
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
"embed"
|
"embed"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -120,6 +122,9 @@ type Msg struct {
|
||||||
|
|
||||||
// noDefaultUserAgent indicates whether the default User Agent will be excluded for the Msg when it's sent.
|
// noDefaultUserAgent indicates whether the default User Agent will be excluded for the Msg when it's sent.
|
||||||
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
|
// SendmailPath is the default system path to the sendmail binary
|
||||||
|
@ -202,6 +207,16 @@ 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)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.sMime = sMime
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// SetCharset sets the encoding charset of the Msg
|
// SetCharset sets the encoding charset of the Msg
|
||||||
func (m *Msg) SetCharset(c Charset) {
|
func (m *Msg) SetCharset(c Charset) {
|
||||||
m.charset = c
|
m.charset = c
|
||||||
|
@ -979,10 +994,47 @@ 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) {
|
||||||
|
currentPart := m.GetParts()[0]
|
||||||
|
currentPart.SetEncoding(EncodingUSASCII)
|
||||||
|
currentPart.SetContentType(TypeTextPlain)
|
||||||
|
content, err := currentPart.GetContent()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("failed to extract content from part")
|
||||||
|
}
|
||||||
|
|
||||||
|
signedContent, err := m.sMime.Sign(content)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.New("failed to sign message")
|
||||||
|
}
|
||||||
|
|
||||||
|
signedPart := msg.newPart(
|
||||||
|
typeSMimeSigned,
|
||||||
|
WithPartEncoding(EncodingB64),
|
||||||
|
WithContentDisposition(DispositionSMime),
|
||||||
|
)
|
||||||
|
signedPart.SetContent(*signedContent)
|
||||||
|
msg.parts = append(msg.parts, signedPart)
|
||||||
|
|
||||||
|
return msg, nil
|
||||||
|
}
|
||||||
|
|
||||||
// WriteTo writes the formated Msg into a give io.Writer and satisfies the io.WriteTo interface
|
// 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) {
|
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.sMime != nil {
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1177,6 +1229,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
|
||||||
func (m *Msg) hasRelated() bool {
|
func (m *Msg) hasRelated() bool {
|
||||||
return m.pgptype == 0 && ((len(m.parts) > 0 && len(m.embeds) > 0) || len(m.embeds) > 1)
|
return m.pgptype == 0 && ((len(m.parts) > 0 && len(m.embeds) > 0) || len(m.embeds) > 1)
|
||||||
|
|
30
msg_test.go
30
msg_test.go
|
@ -3246,6 +3246,36 @@ func TestNewMsgWithNoDefaultUserAgent(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestWithSMimeSinging_ValidPrivateKey tests WithSMimeSinging with given privateKey
|
||||||
|
func TestWithSMimeSinging_ValidPrivateKey(t *testing.T) {
|
||||||
|
privateKey, err := getDummyPrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to load dummy private key: %s", 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 {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestWithSMimeSinging_InvalidPrivateKey tests WithSMimeSinging with given invalid privateKey
|
||||||
|
func TestWithSMimeSinging_InvalidPrivateKey(t *testing.T) {
|
||||||
|
m := NewMsg()
|
||||||
|
|
||||||
|
err := m.SignWithSMime(nil, nil)
|
||||||
|
if !errors.Is(err, ErrInvalidPrivateKey) {
|
||||||
|
t.Errorf("failed to check sMimeAuthConfig 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")
|
||||||
|
|
|
@ -88,6 +88,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)
|
||||||
|
@ -96,7 +100,7 @@ func (mw *msgWriter) writeMsg(msg *Msg) {
|
||||||
mw.startMP(MIMERelated, msg.boundary)
|
mw.startMP(MIMERelated, msg.boundary)
|
||||||
mw.writeString(DoubleNewLine)
|
mw.writeString(DoubleNewLine)
|
||||||
}
|
}
|
||||||
if msg.hasAlt() {
|
if msg.hasAlt() && !msg.hasSMime() {
|
||||||
mw.startMP(MIMEAlternative, msg.boundary)
|
mw.startMP(MIMEAlternative, msg.boundary)
|
||||||
mw.writeString(DoubleNewLine)
|
mw.writeString(DoubleNewLine)
|
||||||
}
|
}
|
||||||
|
|
18
part.go
18
part.go
|
@ -20,6 +20,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 byte slice
|
// 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
|
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
|
||||||
func (p *Part) SetContent(content string) {
|
func (p *Part) SetContent(content string) {
|
||||||
buffer := bytes.NewBufferString(content)
|
buffer := bytes.NewBufferString(content)
|
||||||
|
@ -82,6 +88,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
|
||||||
func (p *Part) SetWriteFunc(writeFunc func(io.Writer) (int64, error)) {
|
func (p *Part) SetWriteFunc(writeFunc func(io.Writer) (int64, error)) {
|
||||||
p.writeFunc = writeFunc
|
p.writeFunc = writeFunc
|
||||||
|
@ -113,3 +124,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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
44
part_test.go
44
part_test.go
|
@ -102,6 +102,24 @@ func TestPart_WithPartContentDescription(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestPart_WithSMimeSinging tests the WithSMimeSinging method
|
||||||
|
func TestPart_WithSMimeSinging(t *testing.T) {
|
||||||
|
m := NewMsg()
|
||||||
|
part := m.newPart(TypeTextPlain, WithSMimeSinging())
|
||||||
|
if part == nil {
|
||||||
|
t.Errorf("newPart() WithSMimeSinging() failed: no part returned")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if part.smime != true {
|
||||||
|
t.Errorf("newPart() WithSMimeSinging() failed: expected: %v, got: %v", true, part.smime)
|
||||||
|
}
|
||||||
|
part.smime = true
|
||||||
|
part.SetIsSMimeSigned(false)
|
||||||
|
if part.smime != false {
|
||||||
|
t.Errorf("newPart() SetIsSMimeSigned() failed: expected: %v, got: %v", false, part.smime)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestPartContentType tests Part.SetContentType
|
// TestPartContentType tests Part.SetContentType
|
||||||
func TestPart_SetContentType(t *testing.T) {
|
func TestPart_SetContentType(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
@ -245,6 +263,32 @@ 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()
|
||||||
|
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ß"
|
||||||
|
|
70
sime.go
Normal file
70
sime.go
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"go.mozilla.org/pkcs7"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrInvalidPrivateKey should be used if private key is invalid
|
||||||
|
ErrInvalidPrivateKey = errors.New("invalid private key")
|
||||||
|
|
||||||
|
// ErrInvalidCertificate should be used if certificate is invalid
|
||||||
|
ErrInvalidCertificate = errors.New("invalid certificate")
|
||||||
|
|
||||||
|
// ErrCouldNotInitialize should be used if the signed data could not initialize
|
||||||
|
ErrCouldNotInitialize = errors.New("could not initialize signed data")
|
||||||
|
|
||||||
|
// ErrCouldNotAddSigner should be used if the signer could not be added
|
||||||
|
ErrCouldNotAddSigner = errors.New("could not add signer message")
|
||||||
|
|
||||||
|
// ErrCouldNotFinishSigning should be used if the signing could not be finished
|
||||||
|
ErrCouldNotFinishSigning = errors.New("could not finish signing")
|
||||||
|
)
|
||||||
|
|
||||||
|
// SMime is used to sign messages with S/MIME
|
||||||
|
type SMime struct {
|
||||||
|
privateKey *rsa.PrivateKey
|
||||||
|
certificate *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
|
||||||
|
}
|
||||||
|
|
||||||
|
if certificate == nil {
|
||||||
|
return nil, ErrInvalidCertificate
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SMime{
|
||||||
|
privateKey: privateKey,
|
||||||
|
certificate: certificate,
|
||||||
|
}, 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)
|
||||||
|
|
||||||
|
toBeSigned.SetDigestAlgorithm(pkcs7.OIDDigestAlgorithmSHA256)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrCouldNotInitialize
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = toBeSigned.AddSigner(sm.certificate, sm.privateKey, pkcs7.SignerInfoConfig{}); err != nil {
|
||||||
|
return nil, ErrCouldNotAddSigner
|
||||||
|
}
|
||||||
|
|
||||||
|
signed, err := toBeSigned.Finish()
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrCouldNotFinishSigning
|
||||||
|
}
|
||||||
|
|
||||||
|
signedData := string(signed)
|
||||||
|
|
||||||
|
return &signedData, nil
|
||||||
|
}
|
41
util_test.go
Normal file
41
util_test.go
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
package mail
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"math/big"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getDummyPrivateKey() (*rsa.PrivateKey, error) {
|
||||||
|
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
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
|
||||||
|
}
|
Loading…
Reference in a new issue