mirror of
https://github.com/wneessen/go-mail.git
synced 2024-12-22 18:50:37 +01:00
Merge pull request #75 from wneessen/fix/74-messageid-generation-not-random-enough
Fixed SetMessageID message ID generation
This commit is contained in:
commit
4250eaf911
4 changed files with 170 additions and 7 deletions
12
msg.go
12
msg.go
|
@ -12,7 +12,6 @@ import (
|
|||
"fmt"
|
||||
ht "html/template"
|
||||
"io"
|
||||
"math/rand"
|
||||
"mime"
|
||||
"net/mail"
|
||||
"os"
|
||||
|
@ -357,12 +356,11 @@ func (m *Msg) SetMessageID() {
|
|||
if err != nil {
|
||||
hn = "localhost.localdomain"
|
||||
}
|
||||
ct := time.Now().Unix()
|
||||
r := rand.New(rand.NewSource(ct))
|
||||
rn := r.Int()
|
||||
pid := os.Getpid()
|
||||
|
||||
mid := fmt.Sprintf("%d.%d.%d@%s", pid, rn, ct, hn)
|
||||
rn, _ := randNum(100000000)
|
||||
rm, _ := randNum(10000)
|
||||
rs, _ := randomStringSecure(25)
|
||||
pid := os.Getpid() * rm
|
||||
mid := fmt.Sprintf("%d.%d%d.%s@%s", pid, rn, rm, rs, hn)
|
||||
m.SetMessageIDWithValue(mid)
|
||||
}
|
||||
|
||||
|
|
20
msg_test.go
20
msg_test.go
|
@ -695,6 +695,26 @@ func TestMsg_SetMessageIDWithValue(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestMsg_SetMessageIDRandomness tests the randomness of Msg.SetMessageID methods
|
||||
func TestMsg_SetMessageIDRandomness(t *testing.T) {
|
||||
var mids []string
|
||||
for i := 0; i < 100; i++ {
|
||||
m := NewMsg()
|
||||
m.SetMessageID()
|
||||
mid := m.GetGenHeader(HeaderMessageID)
|
||||
mids = append(mids, mid[0])
|
||||
}
|
||||
c := make(map[string]int)
|
||||
for i := range mids {
|
||||
c[mids[i]]++
|
||||
}
|
||||
for k, v := range c {
|
||||
if v > 1 {
|
||||
t.Errorf("MessageID randomness not given. MessageID %q was generated %d times", k, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestMsg_FromFormat tests the FromFormat and EnvelopeFrom methods for the Msg object
|
||||
func TestMsg_FromFormat(t *testing.T) {
|
||||
tests := []struct {
|
||||
|
|
75
random.go
Normal file
75
random.go
Normal file
|
@ -0,0 +1,75 @@
|
|||
// SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Range of characters for the secure string generation
|
||||
const cr = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
|
||||
|
||||
// Bitmask sizes for the string generators (based on 93 chars total)
|
||||
const (
|
||||
letterIdxBits = 7 // 7 bits to represent a letter index
|
||||
letterIdxMask = 1<<letterIdxBits - 1 // All 1-bits, as many as letterIdxBits
|
||||
letterIdxMax = 63 / letterIdxBits // # of letter indices fitting in 63 bits
|
||||
)
|
||||
|
||||
// randomStringSecure returns a random, n long string of characters. The character set is based
|
||||
// on the s (special chars) and h (human readable) boolean arguments. This method uses the
|
||||
// crypto/random package and therfore is cryptographically secure
|
||||
func randomStringSecure(n int) (string, error) {
|
||||
rs := strings.Builder{}
|
||||
rs.Grow(n)
|
||||
crl := len(cr)
|
||||
|
||||
rp := make([]byte, 8)
|
||||
_, err := rand.Read(rp)
|
||||
if err != nil {
|
||||
return rs.String(), err
|
||||
}
|
||||
for i, c, r := n-1, binary.BigEndian.Uint64(rp), letterIdxMax; i >= 0; {
|
||||
if r == 0 {
|
||||
_, err := rand.Read(rp)
|
||||
if err != nil {
|
||||
return rs.String(), err
|
||||
}
|
||||
c, r = binary.BigEndian.Uint64(rp), letterIdxMax
|
||||
}
|
||||
if idx := int(c & letterIdxMask); idx < crl {
|
||||
rs.WriteByte(cr[idx])
|
||||
i--
|
||||
}
|
||||
c >>= letterIdxBits
|
||||
r--
|
||||
}
|
||||
|
||||
return rs.String(), nil
|
||||
}
|
||||
|
||||
// randNum returns a random number with a maximum value of n
|
||||
func randNum(n int) (int, error) {
|
||||
if n <= 0 {
|
||||
return 0, fmt.Errorf("provided number is <= 0: %d", n)
|
||||
}
|
||||
mbi := big.NewInt(int64(n))
|
||||
if !mbi.IsUint64() {
|
||||
return 0, fmt.Errorf("big.NewInt() generation returned negative value: %d", mbi)
|
||||
}
|
||||
rn64, err := rand.Int(rand.Reader, mbi)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
rn := int(rn64.Int64())
|
||||
if rn < 0 {
|
||||
return 0, fmt.Errorf("generated random number does not fit as int64: %d", rn64)
|
||||
}
|
||||
return rn, nil
|
||||
}
|
70
random_test.go
Normal file
70
random_test.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
// SPDX-FileCopyrightText: 2022 Winni Neessen <winni@neessen.dev>
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package mail
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestRandomStringSecure tests the randomStringSecure method
|
||||
func TestRandomStringSecure(t *testing.T) {
|
||||
tt := []struct {
|
||||
testName string
|
||||
length int
|
||||
mustNotMatch string
|
||||
}{
|
||||
{"20 chars", 20, "'"},
|
||||
{"100 chars", 100, "'"},
|
||||
{"1000 chars", 1000, "'"},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
rs, err := randomStringSecure(tc.length)
|
||||
if err != nil {
|
||||
t.Errorf("random string generation failed: %s", err)
|
||||
}
|
||||
if strings.Contains(rs, tc.mustNotMatch) {
|
||||
t.Errorf("random string contains unexpected character. got: %s, not-expected: %s",
|
||||
rs, tc.mustNotMatch)
|
||||
}
|
||||
if len(rs) != tc.length {
|
||||
t.Errorf("random string length does not match. expected: %d, got: %d", tc.length, len(rs))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestRandomNum tests the randomNum method
|
||||
func TestRandomNum(t *testing.T) {
|
||||
tt := []struct {
|
||||
testName string
|
||||
max int
|
||||
}{
|
||||
{"Max: 1", 1},
|
||||
{"Max: 20", 20},
|
||||
{"Max: 50", 50},
|
||||
{"Max: 100", 100},
|
||||
{"Max: 1000", 1000},
|
||||
{"Max: 10000", 10000},
|
||||
{"Max: 100000000", 100000000},
|
||||
}
|
||||
|
||||
for _, tc := range tt {
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
rn, err := randNum(tc.max)
|
||||
if err != nil {
|
||||
t.Errorf("random number generation failed: %s", err)
|
||||
}
|
||||
if rn < 0 {
|
||||
t.Errorf("random number generation failed: %d is smaller than zero", rn)
|
||||
}
|
||||
if rn > tc.max {
|
||||
t.Errorf("random number generation failed: %d is bigger than given value %d", rn, tc.max)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue