summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorShulhan <ms@kilabit.info>2022-02-27 22:24:52 +0700
committerShulhan <ms@kilabit.info>2022-02-28 14:39:40 +0700
commitf8e4959352cd6653b8bdb10c445d06196406006a (patch)
treeccc9ccb52a831f2856dfe2ef4250da19494921fd
parent3a1a2715b25f15fe6403af286f91e2b9168a33c9 (diff)
downloadpakakeh.go-f8e4959352cd6653b8bdb10c445d06196406006a.tar.xz
lib/email: set the Date and Message-ID on Message Pack
Calling Pack now set the Date header if its not exist, using the local time; and the message-id header if its not exist using the following format: <epoch>.<random-8-chars>@<local-hostname> The random-8-chars is Seed-ed from Epoch(), so does the boundary.
-rw-r--r--lib/email/email.go19
-rw-r--r--lib/email/email_test.go9
-rw-r--r--lib/email/header.go6
-rw-r--r--lib/email/message.go54
-rw-r--r--lib/email/message_test.go61
-rw-r--r--lib/smtp/mail_tx_example_test.go26
6 files changed, 139 insertions, 36 deletions
diff --git a/lib/email/email.go b/lib/email/email.go
index 89eb1ffc..00aa692d 100644
--- a/lib/email/email.go
+++ b/lib/email/email.go
@@ -4,7 +4,12 @@
package email
-import "time"
+import (
+ "math/rand"
+ "time"
+
+ "github.com/shuLhan/share/lib/ascii"
+)
const (
contentTypeMultipartAlternative = "multipart/alternative"
@@ -20,19 +25,25 @@ const (
lf byte = '\n'
)
+var boundSeps = []byte{'-', '-'}
+
// dateInUtc if set to true, the Date header will be set to UTC instead of
// local time.
-// This variable is used to make test work on all zones.
+// This variable is used to make testing works on all zones.
var dateInUtc bool
//
// Epoch return the UNIX timestamp in seconds.
//
-// This variable is exported to allow function that use date and/or time can
+// This variable is exported to allow function that use time and math/rand can
// be tested with fixed, predictable value.
//
var Epoch = func() int64 {
return time.Now().Unix()
}
-var boundSeps = []byte{'-', '-'}
+// randomChars generate n random characters.
+func randomChars(n int) []byte {
+ rand.Seed(Epoch())
+ return ascii.Random([]byte(ascii.LettersNumber), n)
+}
diff --git a/lib/email/email_test.go b/lib/email/email_test.go
index c0ff81ea..2c4e5fbd 100644
--- a/lib/email/email_test.go
+++ b/lib/email/email_test.go
@@ -9,6 +9,7 @@ import (
"crypto/x509"
"encoding/pem"
"io/ioutil"
+ "os"
"testing"
"time"
@@ -20,6 +21,14 @@ var (
publicKey *rsa.PublicKey
)
+func TestMain(m *testing.M) {
+ Epoch = func() int64 {
+ return 1645811431
+ }
+
+ os.Exit(m.Run())
+}
+
func initKeys(t *testing.T) {
rsaPrivateRaw, err := ioutil.ReadFile("dkim/testdata/rsa.private.pem")
if err != nil {
diff --git a/lib/email/header.go b/lib/email/header.go
index d16030be..97a2992c 100644
--- a/lib/email/header.go
+++ b/lib/email/header.go
@@ -9,8 +9,6 @@ import (
"fmt"
"io"
"log"
-
- "github.com/shuLhan/share/lib/ascii"
)
//
@@ -274,7 +272,7 @@ func (hdr *Header) SetMultipart() (err error) {
return fmt.Errorf("email.SetMultipart: %w", err)
}
- boundary := ascii.Random([]byte(ascii.Hexaletters), 32)
+ boundary := randomChars(32)
contentType := hdr.ContentType()
contentType.SetBoundary(boundary)
@@ -294,6 +292,8 @@ func (hdr *Header) WriteTo(w io.Writer) (n int, err error) {
for _, f = range hdr.fields {
if f.Type == FieldTypeContentType {
m, err = fmt.Fprintf(w, "%s: %s\r\n", f.Name, f.ContentType.String())
+ } else if f.Type == FieldTypeMessageID {
+ m, err = fmt.Fprintf(w, "%s: <%s>\r\n", f.Name, f.oriValue)
} else {
m, err = fmt.Fprintf(w, "%s: %s", f.Name, f.Value)
}
diff --git a/lib/email/message.go b/lib/email/message.go
index df6baf20..731b8bf8 100644
--- a/lib/email/message.go
+++ b/lib/email/message.go
@@ -9,6 +9,7 @@ import (
"crypto/rsa"
"fmt"
"io/ioutil"
+ "os"
"strings"
"time"
@@ -379,6 +380,18 @@ func (msg *Message) SetFrom(mailbox string) (err error) {
}
//
+// SetID set or replace the message-id header to id.
+// If the id is empty, nothing will changes.
+//
+func (msg *Message) SetID(id string) {
+ id = strings.TrimSpace(id)
+ if len(id) == 0 {
+ return
+ }
+ _ = msg.Header.Set(FieldTypeMessageID, []byte(id))
+}
+
+//
// SetSubject set or replace the subject.
// It will do nothing if the subject is empty.
//
@@ -489,6 +502,12 @@ func (msg *Message) CanonHeader(subHeader *Header, dkimField *Field) []byte {
//
// Pack the message for sending.
//
+// This method will set the Date header if its not exist, using the local
+// time;
+// and the message-id header if its not exist using the following format:
+//
+// <epoch>.<random-8-chars>@<local-hostname>
+//
// The message content type is automatically set based on the Body parts.
// If the Body only contain text part, the generated content-type will be set
// to text/plain.
@@ -498,17 +517,46 @@ func (msg *Message) CanonHeader(subHeader *Header, dkimField *Field) []byte {
// set to multipart/alternative.
//
func (msg *Message) Pack() (out []byte, err error) {
+ // TODO: check from, to, subject.
+
var (
- logp = "Pack"
+ logp = "Pack"
+ timeNow = time.Unix(Epoch(), 0)
+
+ dateValue string
+ hostname string
+ id string
+ fields []*Field
)
if len(msg.Body.Parts) == 0 {
return nil, fmt.Errorf("%s: empty body", logp)
}
- // TODO: check date, from, to, subject.
+ fields = msg.Header.Filter(FieldTypeDate)
+ if len(fields) == 0 {
+ if dateInUtc {
+ dateValue = timeNow.UTC().Format(DateFormat)
+ } else {
+ dateValue = timeNow.Format(DateFormat)
+ }
+ err = msg.Header.Set(FieldTypeDate, []byte(dateValue))
+ if err != nil {
+ return nil, fmt.Errorf("%s: %w", logp, err)
+ }
+ }
+
+ fields = msg.Header.Filter(FieldTypeMessageID)
+ if len(fields) == 0 {
+ hostname, err = os.Hostname()
+ if err != nil {
+ return nil, fmt.Errorf("%s: %w", logp, err)
+ }
+ id = fmt.Sprintf("%d.%s@%s", timeNow.Unix(), randomChars(8), hostname)
+ msg.SetID(id)
+ }
- if len(msg.Body.Parts) > 1 {
+ if len(msg.Body.Parts) >= 2 {
out, err = msg.packMultipartAlternative()
if err != nil {
return nil, fmt.Errorf("%s: %w", logp, err)
diff --git a/lib/email/message_test.go b/lib/email/message_test.go
index 599f9a98..7a52a0c2 100644
--- a/lib/email/message_test.go
+++ b/lib/email/message_test.go
@@ -6,7 +6,7 @@ package email
import (
"io/ioutil"
- "math/rand"
+ "os"
"testing"
"github.com/shuLhan/share/lib/email/dkim"
@@ -15,16 +15,18 @@ import (
func TestNewMultipart(t *testing.T) {
var (
- gotMsg *Message
- msgb []byte
- err error
+ gotMsg *Message
+ hostname string
+ msgb []byte
+ err error
)
dateInUtc = true
- Epoch = func() int64 {
- return 1645811431
+
+ hostname, err = os.Hostname()
+ if err != nil {
+ t.Fatal(err)
}
- rand.Seed(42)
cases := []struct {
expMsg string
@@ -45,21 +47,22 @@ func TestNewMultipart(t *testing.T) {
"to: d@e.f\r\n" +
"subject: test\r\n" +
"mime-version: 1.0\r\n" +
- "content-type: multipart/alternative; boundary=1b4df158039f7cce49f0a64b0ea7b7dd\r\n" +
+ "content-type: multipart/alternative; boundary=bqOnpYF7Yw1N0jDpjM004riRyz7oPxD6\r\n" +
+ "message-id: <1645811431.bqOnpYF7@" + hostname + ">\r\n" +
"\r\n" +
- "--1b4df158039f7cce49f0a64b0ea7b7dd\r\n" +
+ "--bqOnpYF7Yw1N0jDpjM004riRyz7oPxD6\r\n" +
"mime-version: 1.0\r\n" +
"content-type: text/plain; charset=\"utf-8\"\r\n" +
"content-transfer-encoding: quoted-printable\r\n" +
"\r\n" +
"This is plain text\r\n" +
- "--1b4df158039f7cce49f0a64b0ea7b7dd\r\n" +
+ "--bqOnpYF7Yw1N0jDpjM004riRyz7oPxD6\r\n" +
"mime-version: 1.0\r\n" +
"content-type: text/html; charset=\"utf-8\"\r\n" +
"content-transfer-encoding: quoted-printable\r\n" +
"\r\n" +
"<b>This is body in HTML</b>\r\n" +
- "--1b4df158039f7cce49f0a64b0ea7b7dd--\r\n",
+ "--bqOnpYF7Yw1N0jDpjM004riRyz7oPxD6--\r\n",
}}
for _, c := range cases {
@@ -326,16 +329,25 @@ func TestMessage_packSingle(t *testing.T) {
}
var (
- msg Message
- err error
- cases []testCase
- got []byte
+ msg Message
+ hostname string
+ err error
+ cases []testCase
+ got []byte
)
+ dateInUtc = true
+ hostname, err = os.Hostname()
+ if err != nil {
+ t.Fatal(err)
+ }
+
cases = []testCase{{
desc: "With body text",
bodyText: []byte(`this is a body text`),
exp: "" +
+ "date: Fri, 25 Feb 2022 17:50:31 +0000\r\n" +
+ "message-id: <1645811431.bqOnpYF7@" + hostname + ">\r\n" +
"mime-version: 1.0\r\n" +
"content-type: text/plain; charset=\"utf-8\"\r\n" +
"content-transfer-encoding: quoted-printable\r\n" +
@@ -345,6 +357,8 @@ func TestMessage_packSingle(t *testing.T) {
desc: "With body HTML",
bodyHtml: []byte(`<p>this is an HTML body</p>`),
exp: "" +
+ "date: Fri, 25 Feb 2022 17:50:31 +0000\r\n" +
+ "message-id: <1645811431.bqOnpYF7@" + hostname + ">\r\n" +
"mime-version: 1.0\r\n" +
"content-type: text/html; charset=\"utf-8\"\r\n" +
"content-transfer-encoding: quoted-printable\r\n" +
@@ -377,9 +391,18 @@ func TestMessage_packSingle(t *testing.T) {
func TestMessage_SetBodyText(t *testing.T) {
var (
- msg Message
- err error
+ msg Message
+ hostname string
+ err error
)
+
+ dateInUtc = true
+
+ hostname, err = os.Hostname()
+ if err != nil {
+ t.Fatal(err)
+ }
+
cases := []struct {
desc string
expMsg string
@@ -388,6 +411,8 @@ func TestMessage_SetBodyText(t *testing.T) {
desc: "With empty Body",
content: []byte("text body"),
expMsg: "" +
+ "date: Fri, 25 Feb 2022 17:50:31 +0000\r\n" +
+ "message-id: <1645811431.bqOnpYF7@" + hostname + ">\r\n" +
"mime-version: 1.0\r\n" +
"content-type: text/plain; charset=\"utf-8\"\r\n" +
"content-transfer-encoding: quoted-printable\r\n" +
@@ -397,6 +422,8 @@ func TestMessage_SetBodyText(t *testing.T) {
desc: "With new text",
content: []byte("new text body"),
expMsg: "" +
+ "date: Fri, 25 Feb 2022 17:50:31 +0000\r\n" +
+ "message-id: <1645811431.bqOnpYF7@" + hostname + ">\r\n" +
"mime-version: 1.0\r\n" +
"content-type: text/plain; charset=\"utf-8\"\r\n" +
"content-transfer-encoding: quoted-printable\r\n" +
diff --git a/lib/smtp/mail_tx_example_test.go b/lib/smtp/mail_tx_example_test.go
index 33ebbee3..a4b6df0a 100644
--- a/lib/smtp/mail_tx_example_test.go
+++ b/lib/smtp/mail_tx_example_test.go
@@ -8,7 +8,7 @@ import (
"bytes"
"fmt"
"log"
- "math/rand"
+ "os"
"regexp"
"time"
@@ -39,12 +39,12 @@ func ExampleNewMailTx() {
mboxes []*email.Mailbox
msg *email.Message
mailtx *MailTx
+ reDate *regexp.Regexp
+ hostname string
data []byte
err error
)
- rand.Seed(42)
-
mboxes, err = email.ParseMailboxes(toAddresses)
if err != nil {
log.Fatal(err)
@@ -75,15 +75,22 @@ func ExampleNewMailTx() {
fmt.Printf("Tx Recipients: %s\n", mailtx.Recipients)
// In order to make the example Output works, we need to replace all
- // CRLF with LF and "date:" with the system timezone.
+ // CRLF with LF, "date:" with the system timezone, and message-id
+ // hostname with fixed "hostname".
data = bytes.ReplaceAll(mailtx.Data, []byte("\r\n"), []byte("\n"))
//fmt.Printf("timeNowUtc: %s\n", timeNowUtc)
//fmt.Printf("dateNowUtc: %s\n", dateNowUtc)
- reDate := regexp.MustCompile(`^date: Wed(.*) \+....`)
+ reDate = regexp.MustCompile(`^date: Wed(.*) \+....`)
data = reDate.ReplaceAll(data, []byte(`date: `+dateNowUtc))
+ hostname, err = os.Hostname()
+ if err != nil {
+ log.Fatal(err)
+ }
+ data = bytes.Replace(data, []byte(hostname), []byte("hostname"), 1)
+
fmt.Printf("Tx Data:\n%s", data)
//Output:
//Tx From: Postmaster <postmaster@mail.example.com>
@@ -94,19 +101,20 @@ func ExampleNewMailTx() {
//to: John <john@example.com>, Jane <jane@example.com>
//subject: Example subject
//mime-version: 1.0
- //content-type: multipart/alternative; boundary=1b4df158039f7cce49f0a64b0ea7b7dd
+ //content-type: multipart/alternative; boundary=QoqDPQfzDVkv5R49vrA78GmqPmlfmBHf
+ //message-id: <1645600000.QoqDPQfz@hostname>
//
- //--1b4df158039f7cce49f0a64b0ea7b7dd
+ //--QoqDPQfzDVkv5R49vrA78GmqPmlfmBHf
//mime-version: 1.0
//content-type: text/plain; charset="utf-8"
//content-transfer-encoding: quoted-printable
//
//Email body as plain text
- //--1b4df158039f7cce49f0a64b0ea7b7dd
+ //--QoqDPQfzDVkv5R49vrA78GmqPmlfmBHf
//mime-version: 1.0
//content-type: text/html; charset="utf-8"
//content-transfer-encoding: quoted-printable
//
//Email body as <b>HTML</b>
- //--1b4df158039f7cce49f0a64b0ea7b7dd--
+ //--QoqDPQfzDVkv5R49vrA78GmqPmlfmBHf--
}