Made File struct more flexible
The File can now be streamed to the SMTP server directly without being buffered into memory first. Fixes #4
This commit is contained in:
parent
66c8b9ae4c
commit
9d308546b7
55
message.go
55
message.go
|
@ -8,6 +8,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"mime"
|
"mime"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
@ -220,38 +221,40 @@ func (msg *Message) AddAlternativeWriter(contentType string, f func(io.Writer) e
|
||||||
|
|
||||||
// A File represents a file that can be attached or embedded in an email.
|
// A File represents a file that can be attached or embedded in an email.
|
||||||
type File struct {
|
type File struct {
|
||||||
Name string
|
// Name represents the base name of the file. If the file is attached to the
|
||||||
MimeType string
|
// message it is the name of the attachment.
|
||||||
Content []byte
|
Name string
|
||||||
ContentID string
|
// Header represents the MIME header of the message part that contains the
|
||||||
|
// file content.
|
||||||
|
Header map[string][]string
|
||||||
|
// Copier is a function run when the message is sent. It should copy the
|
||||||
|
// content of the file to w.
|
||||||
|
Copier func(w io.Writer) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenFile opens a file on disk to create a gomail.File.
|
// NewFile creates a File from the given filename.
|
||||||
func OpenFile(filename string) (*File, error) {
|
func NewFile(filename string) *File {
|
||||||
content, err := readFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
f := CreateFile(filepath.Base(filename), content)
|
|
||||||
|
|
||||||
return f, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateFile creates a gomail.File from the given name and content.
|
|
||||||
func CreateFile(name string, content []byte) *File {
|
|
||||||
mimeType := mime.TypeByExtension(filepath.Ext(name))
|
|
||||||
if mimeType == "" {
|
|
||||||
mimeType = "application/octet-stream"
|
|
||||||
}
|
|
||||||
|
|
||||||
return &File{
|
return &File{
|
||||||
Name: name,
|
Name: filepath.Base(filename),
|
||||||
MimeType: mimeType,
|
Header: make(map[string][]string),
|
||||||
Content: content,
|
Copier: func(w io.Writer) error {
|
||||||
|
h, err := os.Open(filename)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := io.Copy(w, h); err != nil {
|
||||||
|
h.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return h.Close()
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *File) setHeader(field string, value ...string) {
|
||||||
|
f.Header[field] = value
|
||||||
|
}
|
||||||
|
|
||||||
// Attach attaches the files to the email.
|
// Attach attaches the files to the email.
|
||||||
func (msg *Message) Attach(f ...*File) {
|
func (msg *Message) Attach(f ...*File) {
|
||||||
if msg.attachments == nil {
|
if msg.attachments == nil {
|
||||||
|
|
|
@ -198,11 +198,7 @@ func TestAttachmentOnly(t *testing.T) {
|
||||||
msg := NewMessage()
|
msg := NewMessage()
|
||||||
msg.SetHeader("From", "from@example.com")
|
msg.SetHeader("From", "from@example.com")
|
||||||
msg.SetHeader("To", "to@example.com")
|
msg.SetHeader("To", "to@example.com")
|
||||||
f, err := OpenFile("/tmp/test.pdf")
|
msg.Attach(testFile("/tmp/test.pdf"))
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
msg.Attach(f)
|
|
||||||
|
|
||||||
want := &message{
|
want := &message{
|
||||||
from: "from@example.com",
|
from: "from@example.com",
|
||||||
|
@ -224,7 +220,7 @@ func TestAttachment(t *testing.T) {
|
||||||
msg.SetHeader("From", "from@example.com")
|
msg.SetHeader("From", "from@example.com")
|
||||||
msg.SetHeader("To", "to@example.com")
|
msg.SetHeader("To", "to@example.com")
|
||||||
msg.SetBody("text/plain", "Test")
|
msg.SetBody("text/plain", "Test")
|
||||||
msg.Attach(CreateFile("test.pdf", []byte("Content")))
|
msg.Attach(testFile("/tmp/test.pdf"))
|
||||||
|
|
||||||
want := &message{
|
want := &message{
|
||||||
from: "from@example.com",
|
from: "from@example.com",
|
||||||
|
@ -243,7 +239,7 @@ func TestAttachment(t *testing.T) {
|
||||||
"Content-Disposition: attachment; filename=\"test.pdf\"\r\n" +
|
"Content-Disposition: attachment; filename=\"test.pdf\"\r\n" +
|
||||||
"Content-Transfer-Encoding: base64\r\n" +
|
"Content-Transfer-Encoding: base64\r\n" +
|
||||||
"\r\n" +
|
"\r\n" +
|
||||||
base64.StdEncoding.EncodeToString([]byte("Content")) + "\r\n" +
|
base64.StdEncoding.EncodeToString([]byte("Content of test.pdf")) + "\r\n" +
|
||||||
"--_BOUNDARY_1_--\r\n",
|
"--_BOUNDARY_1_--\r\n",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -254,8 +250,8 @@ func TestAttachmentsOnly(t *testing.T) {
|
||||||
msg := NewMessage()
|
msg := NewMessage()
|
||||||
msg.SetHeader("From", "from@example.com")
|
msg.SetHeader("From", "from@example.com")
|
||||||
msg.SetHeader("To", "to@example.com")
|
msg.SetHeader("To", "to@example.com")
|
||||||
msg.Attach(CreateFile("test.pdf", []byte("Content 1")))
|
msg.Attach(testFile("/tmp/test.pdf"))
|
||||||
msg.Attach(CreateFile("test.zip", []byte("Content 2")))
|
msg.Attach(testFile("/tmp/test.zip"))
|
||||||
|
|
||||||
want := &message{
|
want := &message{
|
||||||
from: "from@example.com",
|
from: "from@example.com",
|
||||||
|
@ -269,13 +265,13 @@ func TestAttachmentsOnly(t *testing.T) {
|
||||||
"Content-Disposition: attachment; filename=\"test.pdf\"\r\n" +
|
"Content-Disposition: attachment; filename=\"test.pdf\"\r\n" +
|
||||||
"Content-Transfer-Encoding: base64\r\n" +
|
"Content-Transfer-Encoding: base64\r\n" +
|
||||||
"\r\n" +
|
"\r\n" +
|
||||||
base64.StdEncoding.EncodeToString([]byte("Content 1")) + "\r\n" +
|
base64.StdEncoding.EncodeToString([]byte("Content of test.pdf")) + "\r\n" +
|
||||||
"--_BOUNDARY_1_\r\n" +
|
"--_BOUNDARY_1_\r\n" +
|
||||||
"Content-Type: application/zip; name=\"test.zip\"\r\n" +
|
"Content-Type: application/zip; name=\"test.zip\"\r\n" +
|
||||||
"Content-Disposition: attachment; filename=\"test.zip\"\r\n" +
|
"Content-Disposition: attachment; filename=\"test.zip\"\r\n" +
|
||||||
"Content-Transfer-Encoding: base64\r\n" +
|
"Content-Transfer-Encoding: base64\r\n" +
|
||||||
"\r\n" +
|
"\r\n" +
|
||||||
base64.StdEncoding.EncodeToString([]byte("Content 2")) + "\r\n" +
|
base64.StdEncoding.EncodeToString([]byte("Content of test.zip")) + "\r\n" +
|
||||||
"--_BOUNDARY_1_--\r\n",
|
"--_BOUNDARY_1_--\r\n",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -287,8 +283,8 @@ func TestAttachments(t *testing.T) {
|
||||||
msg.SetHeader("From", "from@example.com")
|
msg.SetHeader("From", "from@example.com")
|
||||||
msg.SetHeader("To", "to@example.com")
|
msg.SetHeader("To", "to@example.com")
|
||||||
msg.SetBody("text/plain", "Test")
|
msg.SetBody("text/plain", "Test")
|
||||||
msg.Attach(CreateFile("test.pdf", []byte("Content 1")))
|
msg.Attach(testFile("/tmp/test.pdf"))
|
||||||
msg.Attach(CreateFile("test.zip", []byte("Content 2")))
|
msg.Attach(testFile("/tmp/test.zip"))
|
||||||
|
|
||||||
want := &message{
|
want := &message{
|
||||||
from: "from@example.com",
|
from: "from@example.com",
|
||||||
|
@ -307,13 +303,13 @@ func TestAttachments(t *testing.T) {
|
||||||
"Content-Disposition: attachment; filename=\"test.pdf\"\r\n" +
|
"Content-Disposition: attachment; filename=\"test.pdf\"\r\n" +
|
||||||
"Content-Transfer-Encoding: base64\r\n" +
|
"Content-Transfer-Encoding: base64\r\n" +
|
||||||
"\r\n" +
|
"\r\n" +
|
||||||
base64.StdEncoding.EncodeToString([]byte("Content 1")) + "\r\n" +
|
base64.StdEncoding.EncodeToString([]byte("Content of test.pdf")) + "\r\n" +
|
||||||
"--_BOUNDARY_1_\r\n" +
|
"--_BOUNDARY_1_\r\n" +
|
||||||
"Content-Type: application/zip; name=\"test.zip\"\r\n" +
|
"Content-Type: application/zip; name=\"test.zip\"\r\n" +
|
||||||
"Content-Disposition: attachment; filename=\"test.zip\"\r\n" +
|
"Content-Disposition: attachment; filename=\"test.zip\"\r\n" +
|
||||||
"Content-Transfer-Encoding: base64\r\n" +
|
"Content-Transfer-Encoding: base64\r\n" +
|
||||||
"\r\n" +
|
"\r\n" +
|
||||||
base64.StdEncoding.EncodeToString([]byte("Content 2")) + "\r\n" +
|
base64.StdEncoding.EncodeToString([]byte("Content of test.zip")) + "\r\n" +
|
||||||
"--_BOUNDARY_1_--\r\n",
|
"--_BOUNDARY_1_--\r\n",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -324,10 +320,10 @@ func TestEmbedded(t *testing.T) {
|
||||||
msg := NewMessage()
|
msg := NewMessage()
|
||||||
msg.SetHeader("From", "from@example.com")
|
msg.SetHeader("From", "from@example.com")
|
||||||
msg.SetHeader("To", "to@example.com")
|
msg.SetHeader("To", "to@example.com")
|
||||||
f := CreateFile("image1.jpg", []byte("Content 1"))
|
f := testFile("image1.jpg")
|
||||||
f.ContentID = "test-content-id"
|
f.Header["Content-ID"] = []string{"<test-content-id>"}
|
||||||
msg.Embed(f)
|
msg.Embed(f)
|
||||||
msg.Embed(CreateFile("image2.jpg", []byte("Content 2")))
|
msg.Embed(testFile("image2.jpg"))
|
||||||
msg.SetBody("text/plain", "Test")
|
msg.SetBody("text/plain", "Test")
|
||||||
|
|
||||||
want := &message{
|
want := &message{
|
||||||
|
@ -348,14 +344,14 @@ func TestEmbedded(t *testing.T) {
|
||||||
"Content-ID: <test-content-id>\r\n" +
|
"Content-ID: <test-content-id>\r\n" +
|
||||||
"Content-Transfer-Encoding: base64\r\n" +
|
"Content-Transfer-Encoding: base64\r\n" +
|
||||||
"\r\n" +
|
"\r\n" +
|
||||||
base64.StdEncoding.EncodeToString([]byte("Content 1")) + "\r\n" +
|
base64.StdEncoding.EncodeToString([]byte("Content of image1.jpg")) + "\r\n" +
|
||||||
"--_BOUNDARY_1_\r\n" +
|
"--_BOUNDARY_1_\r\n" +
|
||||||
"Content-Type: image/jpeg; name=\"image2.jpg\"\r\n" +
|
"Content-Type: image/jpeg; name=\"image2.jpg\"\r\n" +
|
||||||
"Content-Disposition: inline; filename=\"image2.jpg\"\r\n" +
|
"Content-Disposition: inline; filename=\"image2.jpg\"\r\n" +
|
||||||
"Content-ID: <image2.jpg>\r\n" +
|
"Content-ID: <image2.jpg>\r\n" +
|
||||||
"Content-Transfer-Encoding: base64\r\n" +
|
"Content-Transfer-Encoding: base64\r\n" +
|
||||||
"\r\n" +
|
"\r\n" +
|
||||||
base64.StdEncoding.EncodeToString([]byte("Content 2")) + "\r\n" +
|
base64.StdEncoding.EncodeToString([]byte("Content of image2.jpg")) + "\r\n" +
|
||||||
"--_BOUNDARY_1_--\r\n",
|
"--_BOUNDARY_1_--\r\n",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -368,8 +364,8 @@ func TestFullMessage(t *testing.T) {
|
||||||
msg.SetHeader("To", "to@example.com")
|
msg.SetHeader("To", "to@example.com")
|
||||||
msg.SetBody("text/plain", "¡Hola, señor!")
|
msg.SetBody("text/plain", "¡Hola, señor!")
|
||||||
msg.AddAlternative("text/html", "¡<b>Hola</b>, <i>señor</i>!</h1>")
|
msg.AddAlternative("text/html", "¡<b>Hola</b>, <i>señor</i>!</h1>")
|
||||||
msg.Attach(CreateFile("test.pdf", []byte("Content 1")))
|
msg.Attach(testFile("test.pdf"))
|
||||||
msg.Embed(CreateFile("image.jpg", []byte("Content 2")))
|
msg.Embed(testFile("image.jpg"))
|
||||||
|
|
||||||
want := &message{
|
want := &message{
|
||||||
from: "from@example.com",
|
from: "from@example.com",
|
||||||
|
@ -402,7 +398,7 @@ func TestFullMessage(t *testing.T) {
|
||||||
"Content-ID: <image.jpg>\r\n" +
|
"Content-ID: <image.jpg>\r\n" +
|
||||||
"Content-Transfer-Encoding: base64\r\n" +
|
"Content-Transfer-Encoding: base64\r\n" +
|
||||||
"\r\n" +
|
"\r\n" +
|
||||||
base64.StdEncoding.EncodeToString([]byte("Content 2")) + "\r\n" +
|
base64.StdEncoding.EncodeToString([]byte("Content of image.jpg")) + "\r\n" +
|
||||||
"--_BOUNDARY_2_--\r\n" +
|
"--_BOUNDARY_2_--\r\n" +
|
||||||
"\r\n" +
|
"\r\n" +
|
||||||
"--_BOUNDARY_1_\r\n" +
|
"--_BOUNDARY_1_\r\n" +
|
||||||
|
@ -410,7 +406,7 @@ func TestFullMessage(t *testing.T) {
|
||||||
"Content-Disposition: attachment; filename=\"test.pdf\"\r\n" +
|
"Content-Disposition: attachment; filename=\"test.pdf\"\r\n" +
|
||||||
"Content-Transfer-Encoding: base64\r\n" +
|
"Content-Transfer-Encoding: base64\r\n" +
|
||||||
"\r\n" +
|
"\r\n" +
|
||||||
base64.StdEncoding.EncodeToString([]byte("Content 1")) + "\r\n" +
|
base64.StdEncoding.EncodeToString([]byte("Content of test.pdf")) + "\r\n" +
|
||||||
"--_BOUNDARY_1_--\r\n",
|
"--_BOUNDARY_1_--\r\n",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -582,6 +578,15 @@ func getBoundaries(t *testing.T, count int, msg string) []string {
|
||||||
|
|
||||||
var boundaryRegExp = regexp.MustCompile("boundary=(\\w+)")
|
var boundaryRegExp = regexp.MustCompile("boundary=(\\w+)")
|
||||||
|
|
||||||
|
func testFile(name string) *File {
|
||||||
|
f := NewFile(name)
|
||||||
|
f.Copier = func(w io.Writer) error {
|
||||||
|
_, err := w.Write([]byte("Content of " + filepath.Base(f.Name)))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
|
||||||
func BenchmarkFull(b *testing.B) {
|
func BenchmarkFull(b *testing.B) {
|
||||||
emptyFunc := func(from string, to []string, msg io.WriterTo) error {
|
emptyFunc := func(from string, to []string, msg io.WriterTo) error {
|
||||||
return nil
|
return nil
|
||||||
|
@ -598,8 +603,8 @@ func BenchmarkFull(b *testing.B) {
|
||||||
})
|
})
|
||||||
msg.SetBody("text/plain", "¡Hola, señor!")
|
msg.SetBody("text/plain", "¡Hola, señor!")
|
||||||
msg.AddAlternative("text/html", "<p>¡Hola, señor!</p>")
|
msg.AddAlternative("text/html", "<p>¡Hola, señor!</p>")
|
||||||
msg.Attach(CreateFile("benchmark.txt", []byte("Benchmark")))
|
msg.Attach(testFile("benchmark.txt"))
|
||||||
msg.Embed(CreateFile("benchmark.jpg", []byte("Benchmark")))
|
msg.Embed(testFile("benchmark.jpg"))
|
||||||
|
|
||||||
if err := Send(SendFunc(emptyFunc), msg); err != nil {
|
if err := Send(SendFunc(emptyFunc), msg); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
43
writeto.go
43
writeto.go
|
@ -4,8 +4,10 @@ import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
"mime"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"mime/quotedprintable"
|
"mime/quotedprintable"
|
||||||
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -109,24 +111,35 @@ func (w *messageWriter) closeMultipart() {
|
||||||
|
|
||||||
func (w *messageWriter) addFiles(files []*File, isAttachment bool) {
|
func (w *messageWriter) addFiles(files []*File, isAttachment bool) {
|
||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
h := make(map[string][]string)
|
if _, ok := f.Header["Content-Type"]; !ok {
|
||||||
h["Content-Type"] = []string{f.MimeType + "; name=\"" + f.Name + "\""}
|
mediaType := mime.TypeByExtension(filepath.Ext(f.Name))
|
||||||
h["Content-Transfer-Encoding"] = []string{string(Base64)}
|
if mediaType == "" {
|
||||||
if isAttachment {
|
mediaType = "application/octet-stream"
|
||||||
h["Content-Disposition"] = []string{"attachment; filename=\"" + f.Name + "\""}
|
}
|
||||||
} else {
|
f.setHeader("Content-Type", mediaType+`; name="`+f.Name+`"`)
|
||||||
h["Content-Disposition"] = []string{"inline; filename=\"" + f.Name + "\""}
|
}
|
||||||
if f.ContentID != "" {
|
|
||||||
h["Content-ID"] = []string{"<" + f.ContentID + ">"}
|
if _, ok := f.Header["Content-Transfer-Encoding"]; !ok {
|
||||||
|
f.setHeader("Content-Transfer-Encoding", string(Base64))
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := f.Header["Content-Disposition"]; !ok {
|
||||||
|
var disp string
|
||||||
|
if isAttachment {
|
||||||
|
disp = "attachment"
|
||||||
} else {
|
} else {
|
||||||
h["Content-ID"] = []string{"<" + f.Name + ">"}
|
disp = "inline"
|
||||||
|
}
|
||||||
|
f.setHeader("Content-Disposition", disp+`; filename="`+f.Name+`"`)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isAttachment {
|
||||||
|
if _, ok := f.Header["Content-ID"]; !ok {
|
||||||
|
f.setHeader("Content-ID", "<"+f.Name+">")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
w.writeHeaders(h)
|
w.writeHeaders(f.Header)
|
||||||
w.writeBody(func(w io.Writer) error {
|
w.writeBody(f.Copier, Base64)
|
||||||
_, err := w.Write(f.Content)
|
|
||||||
return err
|
|
||||||
}, Base64)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue