mirror of
https://github.com/wneessen/go-mail.git
synced 2024-11-23 14:10:50 +01:00
Compare commits
No commits in common. "894936092edc287fd9d7f4ce803360894ce8aab2" and "07d9654ce72daab7e98955b20f1eb82a8f563b17" have entirely different histories.
894936092e
...
07d9654ce7
13 changed files with 108 additions and 308 deletions
17
client.go
17
client.go
|
@ -127,6 +127,9 @@ type Client struct {
|
||||||
// smtpAuthType represents the authentication type for SMTP AUTH
|
// smtpAuthType represents the authentication type for SMTP AUTH
|
||||||
smtpAuthType SMTPAuthType
|
smtpAuthType SMTPAuthType
|
||||||
|
|
||||||
|
// SMimeAuthConfig represents the authentication type for s/mime crypto key material
|
||||||
|
sMimeAuthConfig *SMimeAuthConfig
|
||||||
|
|
||||||
// smtpClient is the smtp.Client that is set up when using the Dial*() methods
|
// smtpClient is the smtp.Client that is set up when using the Dial*() methods
|
||||||
smtpClient *smtp.Client
|
smtpClient *smtp.Client
|
||||||
|
|
||||||
|
@ -168,6 +171,9 @@ var (
|
||||||
// ErrInvalidTLSConfig should be used if an empty tls.Config is provided
|
// ErrInvalidTLSConfig should be used if an empty tls.Config is provided
|
||||||
ErrInvalidTLSConfig = errors.New("invalid TLS config")
|
ErrInvalidTLSConfig = errors.New("invalid TLS config")
|
||||||
|
|
||||||
|
// ErrInvalidSMimeAuthConfig should be used if the values in the struct SMimeAuthConfig are empty
|
||||||
|
ErrInvalidSMimeAuthConfig = errors.New("invalid S/MIME authentication config")
|
||||||
|
|
||||||
// ErrNoHostname should be used if a Client has no hostname set
|
// ErrNoHostname should be used if a Client has no hostname set
|
||||||
ErrNoHostname = errors.New("hostname for client cannot be empty")
|
ErrNoHostname = errors.New("hostname for client cannot be empty")
|
||||||
|
|
||||||
|
@ -459,6 +465,17 @@ func WithDialContextFunc(dialCtxFunc DialContextFunc) Option {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithSMimeConfig tells the client to use the provided SMIMEAuth for s/mime crypto key material authentication
|
||||||
|
func WithSMimeConfig(sMimeAuthConfig *SMimeAuthConfig) Option {
|
||||||
|
return func(c *Client) error {
|
||||||
|
if sMimeAuthConfig.Certificate == nil && sMimeAuthConfig.PrivateKey == nil {
|
||||||
|
return ErrInvalidSMimeAuthConfig
|
||||||
|
}
|
||||||
|
c.sMimeAuthConfig = sMimeAuthConfig
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TLSPolicy returns the currently set TLSPolicy as string
|
// TLSPolicy returns the currently set TLSPolicy as string
|
||||||
func (c *Client) TLSPolicy() string {
|
func (c *Client) TLSPolicy() string {
|
||||||
return c.tlspolicy.String()
|
return c.tlspolicy.String()
|
||||||
|
|
|
@ -6,10 +6,15 @@ package mail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"math/big"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
@ -117,7 +122,7 @@ func TestNewClientWithOptions(t *testing.T) {
|
||||||
{"WithDialContextFunc()", WithDialContextFunc(func(ctx context.Context, network, address string) (net.Conn, error) {
|
{"WithDialContextFunc()", WithDialContextFunc(func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}), false},
|
}), false},
|
||||||
|
{"WithSMimeConfig()", WithSMimeConfig(&SMimeAuthConfig{}), true},
|
||||||
{
|
{
|
||||||
"WithDSNRcptNotifyType() NEVER combination",
|
"WithDSNRcptNotifyType() NEVER combination",
|
||||||
WithDSNRcptNotifyType(DSNRcptNotifySuccess, DSNRcptNotifyNever), true,
|
WithDSNRcptNotifyType(DSNRcptNotifySuccess, DSNRcptNotifyNever), true,
|
||||||
|
@ -756,6 +761,43 @@ func TestClient_DialWithContextInvalidAuth(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestWithSMime tests the WithSMime method with invalid SMimeAuthConfig for the Client object
|
||||||
|
func TestWithSMime_InvalidConfig(t *testing.T) {
|
||||||
|
_, err := NewClient(DefaultHost, WithSMimeConfig(&SMimeAuthConfig{}))
|
||||||
|
if !errors.Is(err, ErrInvalidSMimeAuthConfig) {
|
||||||
|
t.Errorf("failed to check sMimeAuthConfig values correctly: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestWithSMime tests the WithSMime method with valid SMimeAuthConfig that is loaded from dummy certificate for the Client object
|
||||||
|
func TestWithSMime_ValidConfig(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)
|
||||||
|
}
|
||||||
|
|
||||||
|
sMimeAuthConfig := &SMimeAuthConfig{PrivateKey: privateKey, Certificate: certificate}
|
||||||
|
c, err := NewClient(DefaultHost, WithSMimeConfig(sMimeAuthConfig))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("failed to create new client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.sMimeAuthConfig != sMimeAuthConfig {
|
||||||
|
t.Errorf("failed to set smeAuthConfig. Expected %v, got: %v", sMimeAuthConfig, c.sMimeAuthConfig)
|
||||||
|
}
|
||||||
|
if c.sMimeAuthConfig.PrivateKey != sMimeAuthConfig.PrivateKey {
|
||||||
|
t.Errorf("failed to set smeAuthConfig.PrivateKey Expected %v, got: %v", sMimeAuthConfig, c.sMimeAuthConfig)
|
||||||
|
}
|
||||||
|
if c.sMimeAuthConfig.Certificate != sMimeAuthConfig.Certificate {
|
||||||
|
t.Errorf("failed to set smeAuthConfig.Certificate Expected %v, got: %v", sMimeAuthConfig, c.sMimeAuthConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestClient_checkConn tests the checkConn method with intentional breaking for the Client object
|
// TestClient_checkConn tests the checkConn method with intentional breaking for the Client object
|
||||||
func TestClient_checkConn(t *testing.T) {
|
func TestClient_checkConn(t *testing.T) {
|
||||||
c, err := getTestConnection(true)
|
c, err := getTestConnection(true)
|
||||||
|
@ -1484,6 +1526,37 @@ func getFakeDialFunc(conn net.Conn) DialContextFunc {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
type faker struct {
|
type faker struct {
|
||||||
io.ReadWriter
|
io.ReadWriter
|
||||||
}
|
}
|
||||||
|
|
15
encoding.go
15
encoding.go
|
@ -19,9 +19,6 @@ type MIMEVersion string
|
||||||
// MIMEType represents the MIME type for the mail
|
// MIMEType represents the MIME type for the mail
|
||||||
type MIMEType string
|
type MIMEType string
|
||||||
|
|
||||||
// Disposition represents a content disposition for the Msg
|
|
||||||
type Disposition string
|
|
||||||
|
|
||||||
// List of supported encodings
|
// List of supported encodings
|
||||||
const (
|
const (
|
||||||
// EncodingB64 represents the Base64 encoding as specified in RFC 2045.
|
// EncodingB64 represents the Base64 encoding as specified in RFC 2045.
|
||||||
|
@ -152,7 +149,6 @@ 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
|
||||||
|
@ -160,12 +156,6 @@ 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`
|
|
||||||
)
|
|
||||||
|
|
||||||
// List of common content disposition
|
|
||||||
const (
|
|
||||||
DispositionSMime Disposition = `attachment; filename="smime.p7s"`
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -182,8 +172,3 @@ 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 Disposition into a printable format
|
|
||||||
func (d Disposition) String() string {
|
|
||||||
return string(d)
|
|
||||||
}
|
|
||||||
|
|
|
@ -61,11 +61,6 @@ 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) {
|
||||||
|
@ -126,22 +121,3 @@ func TestCharset_String(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestDisposition_String tests the string method of the Disposition object
|
|
||||||
func TestDisposition_String(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
d Disposition
|
|
||||||
want string
|
|
||||||
}{
|
|
||||||
{"Disposition: S/Mime", DispositionSMime, `attachment; filename="smime.p7s"`},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if tt.d.String() != tt.want {
|
|
||||||
t.Errorf("wrong string for Disposition returned. Expected: %s, got: %s",
|
|
||||||
tt.want, tt.d.String())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
2
go.mod
2
go.mod
|
@ -5,5 +5,3 @@
|
||||||
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
|
@ -1,2 +0,0 @@
|
||||||
go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI=
|
|
||||||
go.mozilla.org/pkcs7 v0.9.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk=
|
|
61
msg.go
61
msg.go
|
@ -7,8 +7,6 @@ package mail
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/rsa"
|
|
||||||
"crypto/x509"
|
|
||||||
"embed"
|
"embed"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
@ -123,8 +121,8 @@ 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
|
// sMimeSinging indicates whether the message should be singed with S/MIME when it's sent.
|
||||||
sMime *SMime
|
sMimeSinging bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendmailPath is the default system path to the sendmail binary
|
// SendmailPath is the default system path to the sendmail binary
|
||||||
|
@ -207,14 +205,11 @@ func WithNoDefaultUserAgent() MsgOption {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SignWithSMime configures the Msg to be signed with S/MIME
|
// WithSMimeSinging configures the Msg to be S/MIME singed sent.
|
||||||
func (m *Msg) SignWithSMime(privateKey *rsa.PrivateKey, certificate *x509.Certificate) error {
|
func WithSMimeSinging() MsgOption {
|
||||||
sMime, err := NewSMime(privateKey, certificate)
|
return func(m *Msg) {
|
||||||
if err != nil {
|
m.sMimeSinging = true
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
m.sMime = sMime
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetCharset sets the encoding charset of the Msg
|
// SetCharset sets the encoding charset of the Msg
|
||||||
|
@ -983,47 +978,10 @@ 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}
|
||||||
msg := m.applyMiddlewares(m)
|
mw.writeMsg(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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1218,11 +1176,6 @@ 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)
|
||||||
|
|
32
msg_test.go
32
msg_test.go
|
@ -3233,33 +3233,11 @@ func TestNewMsgWithNoDefaultUserAgent(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestWithSMimeSinging_ValidPrivateKey tests WithSMimeSinging with given privateKey
|
// TestWithSMimeSinging tests WithSMimeSinging
|
||||||
func TestWithSMimeSinging_ValidPrivateKey(t *testing.T) {
|
func TestWithSMimeSinging(t *testing.T) {
|
||||||
privateKey, err := getDummyPrivateKey()
|
m := NewMsg(WithSMimeSinging())
|
||||||
if err != nil {
|
if m.sMimeSinging != true {
|
||||||
t.Errorf("failed to load dummy private key: %s", err)
|
t.Errorf("WithSMimeSinging() failed. Expected: %t, got: %t", true, false)
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -88,10 +88,6 @@ 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)
|
||||||
|
@ -100,7 +96,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() && !msg.hasSMime() {
|
if msg.hasAlt() {
|
||||||
mw.startMP(MIMEAlternative, msg.boundary)
|
mw.startMP(MIMEAlternative, msg.boundary)
|
||||||
mw.writeString(DoubleNewLine)
|
mw.writeString(DoubleNewLine)
|
||||||
}
|
}
|
||||||
|
@ -269,9 +265,6 @@ func (mw *msgWriter) writePart(part *Part, charset Charset) {
|
||||||
if part.description != "" {
|
if part.description != "" {
|
||||||
mimeHeader.Add(string(HeaderContentDescription), part.description)
|
mimeHeader.Add(string(HeaderContentDescription), part.description)
|
||||||
}
|
}
|
||||||
if part.disposition != "" {
|
|
||||||
mimeHeader.Add(string(HeaderContentDisposition), part.disposition.String())
|
|
||||||
}
|
|
||||||
mimeHeader.Add(string(HeaderContentType), contentType)
|
mimeHeader.Add(string(HeaderContentType), contentType)
|
||||||
mimeHeader.Add(string(HeaderContentTransferEnc), contentTransferEnc)
|
mimeHeader.Add(string(HeaderContentTransferEnc), contentTransferEnc)
|
||||||
mw.newPart(mimeHeader)
|
mw.newPart(mimeHeader)
|
||||||
|
|
18
part.go
18
part.go
|
@ -17,7 +17,6 @@ type Part struct {
|
||||||
contentType ContentType
|
contentType ContentType
|
||||||
charset Charset
|
charset Charset
|
||||||
description string
|
description string
|
||||||
disposition Disposition
|
|
||||||
encoding Encoding
|
encoding Encoding
|
||||||
isDeleted bool
|
isDeleted bool
|
||||||
writeFunc func(io.Writer) (int64, error)
|
writeFunc func(io.Writer) (int64, error)
|
||||||
|
@ -57,11 +56,6 @@ func (p *Part) GetDescription() string {
|
||||||
return p.description
|
return p.description
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDisposition returns the currently set Content-Disposition of the Part
|
|
||||||
func (p *Part) GetDisposition() Disposition {
|
|
||||||
return p.disposition
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
||||||
|
@ -88,11 +82,6 @@ func (p *Part) SetDescription(description string) {
|
||||||
p.description = description
|
p.description = description
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDisposition overrides the Content-Disposition of the Part
|
|
||||||
func (p *Part) SetDisposition(disposition Disposition) {
|
|
||||||
p.disposition = disposition
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -124,10 +113,3 @@ func WithPartContentDescription(description string) PartOption {
|
||||||
p.description = description
|
p.description = description
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithContentDisposition overrides the default Part Content-Disposition
|
|
||||||
func WithContentDisposition(disposition Disposition) PartOption {
|
|
||||||
return func(p *Part) {
|
|
||||||
p.disposition = disposition
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
54
part_test.go
54
part_test.go
|
@ -102,35 +102,6 @@ func TestPart_WithPartContentDescription(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestPart_withContentDisposition tests the WithContentDisposition method
|
|
||||||
func TestPart_withContentDisposition(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
disposition Disposition
|
|
||||||
}{
|
|
||||||
{"Part disposition: test", "test"},
|
|
||||||
{"Part disposition: empty", ""},
|
|
||||||
}
|
|
||||||
for _, tt := range tests {
|
|
||||||
m := NewMsg()
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
part := m.newPart(TypeTextPlain, WithContentDisposition(tt.disposition), nil)
|
|
||||||
if part == nil {
|
|
||||||
t.Errorf("newPart() WithPartContentDescription() failed: no part returned")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if part.disposition != tt.disposition {
|
|
||||||
t.Errorf("newPart() WithContentDisposition() failed: expected: %s, got: %s", tt.disposition, part.description)
|
|
||||||
}
|
|
||||||
part.disposition = ""
|
|
||||||
part.SetDisposition(tt.disposition)
|
|
||||||
if part.disposition != tt.disposition {
|
|
||||||
t.Errorf("newPart() SetDisposition() failed: expected: %s, got: %s", tt.disposition, part.description)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestPartContentType tests Part.SetContentType
|
// TestPartContentType tests Part.SetContentType
|
||||||
func TestPart_SetContentType(t *testing.T) {
|
func TestPart_SetContentType(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
@ -352,31 +323,6 @@ func TestPart_SetDescription(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestPart_SetDisposition tests Part.SetDisposition
|
|
||||||
func TestPart_SetDisposition(t *testing.T) {
|
|
||||||
c := "This is a test"
|
|
||||||
d := Disposition("test-disposition")
|
|
||||||
m := NewMsg()
|
|
||||||
m.SetBodyString(TypeTextPlain, c)
|
|
||||||
pl, err := getPartList(m)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("failed: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
pd := pl[0].GetDisposition()
|
|
||||||
if pd != "" {
|
|
||||||
t.Errorf("Part.GetDisposition failed. Expected empty description but got: %s", pd)
|
|
||||||
}
|
|
||||||
pl[0].SetDisposition(d)
|
|
||||||
if pl[0].disposition != d {
|
|
||||||
t.Errorf("Part.SetDisposition failed. Expected description to be: %s, got: %s", d, pd)
|
|
||||||
}
|
|
||||||
pd = pl[0].GetDisposition()
|
|
||||||
if pd != d {
|
|
||||||
t.Errorf("Part.GetDisposition failed. Expected: %s, got: %s", d, pd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestPart_Delete tests Part.Delete
|
// TestPart_Delete tests Part.Delete
|
||||||
func TestPart_Delete(t *testing.T) {
|
func TestPart_Delete(t *testing.T) {
|
||||||
c := "This is a test with ümläutß"
|
c := "This is a test with ümläutß"
|
||||||
|
|
66
sime.go
66
sime.go
|
@ -3,68 +3,10 @@ package mail
|
||||||
import (
|
import (
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"errors"
|
|
||||||
"go.mozilla.org/pkcs7"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
// SMimeAuthConfig represents the authentication type for s/mime crypto key material
|
||||||
// ErrInvalidPrivateKey should be used if private key is invalid
|
type SMimeAuthConfig struct {
|
||||||
ErrInvalidPrivateKey = errors.New("invalid private key")
|
Certificate *x509.Certificate
|
||||||
|
PrivateKey *rsa.PrivateKey
|
||||||
// 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
41
util_test.go
|
@ -1,41 +0,0 @@
|
||||||
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