Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

be able to control randomness of boundary headers #276

Merged
merged 21 commits into from
Feb 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"bytes"
"errors"
"io/ioutil"
"math/rand"
"mime"
"net/mail"
"net/textproto"
Expand All @@ -28,13 +29,20 @@ type MailBuilder struct {
text, html []byte
inlines, attachments []*Part
err error
randSource rand.Source
}

// Builder returns an empty MailBuilder struct.
func Builder() MailBuilder {
return MailBuilder{}
}

// RandSeed sets the seed for random uuid boundary strings.
func (p MailBuilder) RandSeed(seed int64) MailBuilder {
p.randSource = stringutil.NewLockedSource(seed)
return p
}

// Error returns the stored error from a file attachment/inline read or nil.
func (p MailBuilder) Error() error {
return p.err
Expand Down Expand Up @@ -391,9 +399,22 @@ func (p MailBuilder) Build() (*Part, error) {
h.Add(k, s)
}
}
if r := p.randSource; r != nil {
root.propagateRand(r)
}
return root, nil
}

func (p *Part) propagateRand(rand rand.Source) {
p.randSource = rand
for _, x := range []*Part{p.FirstChild, p.NextSibling} {
if x == nil {
continue
}
x.propagateRand(rand)
}
}

// SendWithReversePath encodes the message and sends it via the specified Sender.
func (p MailBuilder) SendWithReversePath(sender Sender, from string) error {
buf := &bytes.Buffer{}
Expand Down
2 changes: 1 addition & 1 deletion encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ func (p *Part) setupMIMEHeaders() transferEncoding {
// Setup headers.
if p.FirstChild != nil && p.Boundary == "" {
// Multipart, generate random boundary marker.
p.Boundary = "enmime-" + stringutil.UUID()
p.Boundary = "enmime-" + stringutil.UUID(p.randSource)
}
if p.ContentID != "" {
p.Header.Set(hnContentID, coding.ToIDHeader(p.ContentID))
Expand Down
44 changes: 44 additions & 0 deletions internal/stringutil/rand_source.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package stringutil

import (
"math/rand"
"sync"
"time"
)

var globalRandSource rand.Source

func init() {
globalRandSource = NewLockedSource(time.Now().UTC().UnixNano())
}

// NewLockedSource creates a source of randomness using the given seed.
func NewLockedSource(seed int64) rand.Source64 {
return &lockedSource{
lock: new(sync.Mutex),
s: rand.NewSource(seed).(rand.Source64),
}
}

type lockedSource struct {
lock sync.Locker
jhillyerd marked this conversation as resolved.
Show resolved Hide resolved
s rand.Source64
}

func (x *lockedSource) Int63() int64 {
x.lock.Lock()
defer x.lock.Unlock()
return x.s.Int63()
}

func (x *lockedSource) Uint64() uint64 {
x.lock.Lock()
defer x.lock.Unlock()
return x.s.Uint64()
}

func (x *lockedSource) Seed(seed int64) {
x.lock.Lock()
defer x.lock.Unlock()
x.s.Seed(seed)
}
16 changes: 6 additions & 10 deletions internal/stringutil/uuid.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,15 @@ package stringutil
import (
"fmt"
"math/rand"
"sync"
"time"
)

var uuidRand = rand.New(rand.NewSource(time.Now().UnixNano()))
var uuidMutex = &sync.Mutex{}

// UUID generates a random UUID according to RFC 4122.
func UUID() string {
// UUID generates a random UUID according to RFC 4122, using optional rand if supplied
func UUID(rs rand.Source) string {
uuid := make([]byte, 16)
uuidMutex.Lock()
_, _ = uuidRand.Read(uuid)
uuidMutex.Unlock()
if rs == nil {
rs = globalRandSource
}
_, _ = rand.New(rs).Read(uuid)
// variant bits; see section 4.1.1
uuid[8] = uuid[8]&^0xc0 | 0x80
// version 4 (pseudo-random); see section 4.1.3
Expand Down
4 changes: 2 additions & 2 deletions internal/stringutil/uuid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import (
)

func TestUUID(t *testing.T) {
id1 := stringutil.UUID()
id2 := stringutil.UUID()
id1 := stringutil.UUID(nil)
id2 := stringutil.UUID(nil)

if id1 == id2 {
t.Errorf("Random UUID should not equal another random UUID")
Expand Down
7 changes: 5 additions & 2 deletions part.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"encoding/base64"
"io"
"io/ioutil"
"math/rand"
"mime/quotedprintable"
"net/textproto"
"strconv"
Expand All @@ -26,11 +27,11 @@ const (
// Part represents a node in the MIME multipart tree. The Content-Type, Disposition and File Name
// are parsed out of the header for easier access.
type Part struct {
PartID string // PartID labels this parts position within the tree.
PartID string // PartID labels this part's position within the tree.
Parent *Part // Parent of this part (can be nil.)
FirstChild *Part // FirstChild is the top most child of this part.
NextSibling *Part // NextSibling of this part.
Header textproto.MIMEHeader // Header for this Part.
Header textproto.MIMEHeader // Header for this part.

Boundary string // Boundary marker used within this part.
ContentID string // ContentID header for cid URL scheme.
Expand All @@ -47,6 +48,8 @@ type Part struct {
Epilogue []byte // Epilogue contains data following the closing boundary marker.

parser *Parser // Provides access to parsing options.

randSource rand.Source // optional rand for uuid boundary generation
}

// NewPart creates a new Part object.
Expand Down
93 changes: 93 additions & 0 deletions randoption_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package enmime_test

import (
"bytes"
"fmt"
"testing"
"time"

"github.com/jhillyerd/enmime"
"github.com/stretchr/testify/assert"
)

// TestRandOption checks that different randomness modes behave as expected, relative to one another.
func TestRandOption(t *testing.T) {
types := []ReproducibilityMode{ZeroSource, OneSource, DefaultSource, TimestampSource}
for _, a := range types {
for _, b := range types {
ha, hb := buildEmail(t, a), buildEmail(t, b)
if a == b && a.IsReproducible() {
assert.Equal(t, ha, hb)
} else {
assert.NotEqual(t, ha, hb)
}
}
}
}

type ReproducibilityMode int

const (
ZeroSource ReproducibilityMode = iota
OneSource
DefaultSource
TimestampSource
)

func (mode ReproducibilityMode) IsReproducible() bool {
switch mode {
case ZeroSource:
return true
case OneSource:
return true
case DefaultSource:
return false
case TimestampSource:
return false
default:
panic(fmt.Errorf("illegal mode: %d", mode))
}
}

func (mode ReproducibilityMode) String() string {
switch mode {
case ZeroSource:
return "ZeroSource"
case OneSource:
return "OneSource"
case DefaultSource:
return "DefaultSource"
case TimestampSource:
return "TimestampSource"
default:
panic(fmt.Errorf("illegal mode: %d", mode))
}
}

// buildEmail creates a string email, according to the given Reproducibilitymode.
func buildEmail(t *testing.T, mode ReproducibilityMode) string {
t.Helper()
var b enmime.MailBuilder
switch mode {
case ZeroSource:
b = enmime.Builder().RandSeed(0)
case OneSource:
b = enmime.Builder().RandSeed(1)
case DefaultSource:
b = enmime.Builder()
case TimestampSource:
b = enmime.Builder().RandSeed(time.Now().UTC().UnixNano())
default:
panic(fmt.Errorf("illegal mode: %d", mode))
}
b = b.From("name", "same").To("anon", "anon@example.com").AddAttachment([]byte("testing"), "text/plain", "test.txt")
p, err := b.Build()
if err != nil {
t.Fatalf("can't build email: %v", err)
}
w := new(bytes.Buffer)
if err := p.Encode(w); err != nil {
t.Fatalf("can't encode part: %v", err)
}
return w.String()
}