You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							306 lines
						
					
					
						
							6.6 KiB
						
					
					
				
			
		
		
	
	
							306 lines
						
					
					
						
							6.6 KiB
						
					
					
				| package gomail
 | |
| 
 | |
| import (
 | |
| 	"encoding/base64"
 | |
| 	"errors"
 | |
| 	"io"
 | |
| 	"mime"
 | |
| 	"mime/multipart"
 | |
| 	"path/filepath"
 | |
| 	"strings"
 | |
| 	"time"
 | |
| )
 | |
| 
 | |
| // WriteTo implements io.WriterTo. It dumps the whole message into w.
 | |
| func (m *Message) WriteTo(w io.Writer) (int64, error) {
 | |
| 	mw := &messageWriter{w: w}
 | |
| 	mw.writeMessage(m)
 | |
| 	return mw.n, mw.err
 | |
| }
 | |
| 
 | |
| func (w *messageWriter) writeMessage(m *Message) {
 | |
| 	if _, ok := m.header["Mime-Version"]; !ok {
 | |
| 		w.writeString("Mime-Version: 1.0\r\n")
 | |
| 	}
 | |
| 	if _, ok := m.header["Date"]; !ok {
 | |
| 		w.writeHeader("Date", m.FormatDate(now()))
 | |
| 	}
 | |
| 	w.writeHeaders(m.header)
 | |
| 
 | |
| 	if m.hasMixedPart() {
 | |
| 		w.openMultipart("mixed")
 | |
| 	}
 | |
| 
 | |
| 	if m.hasRelatedPart() {
 | |
| 		w.openMultipart("related")
 | |
| 	}
 | |
| 
 | |
| 	if m.hasAlternativePart() {
 | |
| 		w.openMultipart("alternative")
 | |
| 	}
 | |
| 	for _, part := range m.parts {
 | |
| 		w.writePart(part, m.charset)
 | |
| 	}
 | |
| 	if m.hasAlternativePart() {
 | |
| 		w.closeMultipart()
 | |
| 	}
 | |
| 
 | |
| 	w.addFiles(m.embedded, false)
 | |
| 	if m.hasRelatedPart() {
 | |
| 		w.closeMultipart()
 | |
| 	}
 | |
| 
 | |
| 	w.addFiles(m.attachments, true)
 | |
| 	if m.hasMixedPart() {
 | |
| 		w.closeMultipart()
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (m *Message) hasMixedPart() bool {
 | |
| 	return (len(m.parts) > 0 && len(m.attachments) > 0) || len(m.attachments) > 1
 | |
| }
 | |
| 
 | |
| func (m *Message) hasRelatedPart() bool {
 | |
| 	return (len(m.parts) > 0 && len(m.embedded) > 0) || len(m.embedded) > 1
 | |
| }
 | |
| 
 | |
| func (m *Message) hasAlternativePart() bool {
 | |
| 	return len(m.parts) > 1
 | |
| }
 | |
| 
 | |
| type messageWriter struct {
 | |
| 	w          io.Writer
 | |
| 	n          int64
 | |
| 	writers    [3]*multipart.Writer
 | |
| 	partWriter io.Writer
 | |
| 	depth      uint8
 | |
| 	err        error
 | |
| }
 | |
| 
 | |
| func (w *messageWriter) openMultipart(mimeType string) {
 | |
| 	mw := multipart.NewWriter(w)
 | |
| 	contentType := "multipart/" + mimeType + ";\r\n boundary=" + mw.Boundary()
 | |
| 	w.writers[w.depth] = mw
 | |
| 
 | |
| 	if w.depth == 0 {
 | |
| 		w.writeHeader("Content-Type", contentType)
 | |
| 		w.writeString("\r\n")
 | |
| 	} else {
 | |
| 		w.createPart(map[string][]string{
 | |
| 			"Content-Type": {contentType},
 | |
| 		})
 | |
| 	}
 | |
| 	w.depth++
 | |
| }
 | |
| 
 | |
| func (w *messageWriter) createPart(h map[string][]string) {
 | |
| 	w.partWriter, w.err = w.writers[w.depth-1].CreatePart(h)
 | |
| }
 | |
| 
 | |
| func (w *messageWriter) closeMultipart() {
 | |
| 	if w.depth > 0 {
 | |
| 		w.writers[w.depth-1].Close()
 | |
| 		w.depth--
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (w *messageWriter) writePart(p *part, charset string) {
 | |
| 	w.writeHeaders(map[string][]string{
 | |
| 		"Content-Type":              {p.contentType + "; charset=" + charset},
 | |
| 		"Content-Transfer-Encoding": {string(p.encoding)},
 | |
| 	})
 | |
| 	w.writeBody(p.copier, p.encoding)
 | |
| }
 | |
| 
 | |
| func (w *messageWriter) addFiles(files []*file, isAttachment bool) {
 | |
| 	for _, f := range files {
 | |
| 		if _, ok := f.Header["Content-Type"]; !ok {
 | |
| 			mediaType := mime.TypeByExtension(filepath.Ext(f.Name))
 | |
| 			if mediaType == "" {
 | |
| 				mediaType = "application/octet-stream"
 | |
| 			}
 | |
| 			f.setHeader("Content-Type", mediaType+`; name="`+f.Name+`"`)
 | |
| 		}
 | |
| 
 | |
| 		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 {
 | |
| 				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(f.Header)
 | |
| 		w.writeBody(f.CopyFunc, Base64)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (w *messageWriter) Write(p []byte) (int, error) {
 | |
| 	if w.err != nil {
 | |
| 		return 0, errors.New("gomail: cannot write as writer is in error")
 | |
| 	}
 | |
| 
 | |
| 	var n int
 | |
| 	n, w.err = w.w.Write(p)
 | |
| 	w.n += int64(n)
 | |
| 	return n, w.err
 | |
| }
 | |
| 
 | |
| func (w *messageWriter) writeString(s string) {
 | |
| 	n, _ := io.WriteString(w.w, s)
 | |
| 	w.n += int64(n)
 | |
| }
 | |
| 
 | |
| func (w *messageWriter) writeHeader(k string, v ...string) {
 | |
| 	w.writeString(k)
 | |
| 	if len(v) == 0 {
 | |
| 		w.writeString(":\r\n")
 | |
| 		return
 | |
| 	}
 | |
| 	w.writeString(": ")
 | |
| 
 | |
| 	// Max header line length is 78 characters in RFC 5322 and 76 characters
 | |
| 	// in RFC 2047. So for the sake of simplicity we use the 76 characters
 | |
| 	// limit.
 | |
| 	charsLeft := 76 - len(k) - len(": ")
 | |
| 
 | |
| 	for i, s := range v {
 | |
| 		// If the line is already too long, insert a newline right away.
 | |
| 		if charsLeft < 1 {
 | |
| 			if i == 0 {
 | |
| 				w.writeString("\r\n ")
 | |
| 			} else {
 | |
| 				w.writeString(",\r\n ")
 | |
| 			}
 | |
| 			charsLeft = 75
 | |
| 		} else if i != 0 {
 | |
| 			w.writeString(", ")
 | |
| 			charsLeft -= 2
 | |
| 		}
 | |
| 
 | |
| 		// While the header content is too long, fold it by inserting a newline.
 | |
| 		for len(s) > charsLeft {
 | |
| 			s = w.writeLine(s, charsLeft)
 | |
| 			charsLeft = 75
 | |
| 		}
 | |
| 		w.writeString(s)
 | |
| 		if i := lastIndexByte(s, '\n'); i != -1 {
 | |
| 			charsLeft = 75 - (len(s) - i - 1)
 | |
| 		} else {
 | |
| 			charsLeft -= len(s)
 | |
| 		}
 | |
| 	}
 | |
| 	w.writeString("\r\n")
 | |
| }
 | |
| 
 | |
| func (w *messageWriter) writeLine(s string, charsLeft int) string {
 | |
| 	// If there is already a newline before the limit. Write the line.
 | |
| 	if i := strings.IndexByte(s, '\n'); i != -1 && i < charsLeft {
 | |
| 		w.writeString(s[:i+1])
 | |
| 		return s[i+1:]
 | |
| 	}
 | |
| 
 | |
| 	for i := charsLeft - 1; i >= 0; i-- {
 | |
| 		if s[i] == ' ' {
 | |
| 			w.writeString(s[:i])
 | |
| 			w.writeString("\r\n ")
 | |
| 			return s[i+1:]
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// We could not insert a newline cleanly so look for a space or a newline
 | |
| 	// even if it is after the limit.
 | |
| 	for i := 75; i < len(s); i++ {
 | |
| 		if s[i] == ' ' {
 | |
| 			w.writeString(s[:i])
 | |
| 			w.writeString("\r\n ")
 | |
| 			return s[i+1:]
 | |
| 		}
 | |
| 		if s[i] == '\n' {
 | |
| 			w.writeString(s[:i+1])
 | |
| 			return s[i+1:]
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Too bad, no space or newline in the whole string. Just write everything.
 | |
| 	w.writeString(s)
 | |
| 	return ""
 | |
| }
 | |
| 
 | |
| func (w *messageWriter) writeHeaders(h map[string][]string) {
 | |
| 	if w.depth == 0 {
 | |
| 		for k, v := range h {
 | |
| 			if k != "Bcc" {
 | |
| 				w.writeHeader(k, v...)
 | |
| 			}
 | |
| 		}
 | |
| 	} else {
 | |
| 		w.createPart(h)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (w *messageWriter) writeBody(f func(io.Writer) error, enc Encoding) {
 | |
| 	var subWriter io.Writer
 | |
| 	if w.depth == 0 {
 | |
| 		w.writeString("\r\n")
 | |
| 		subWriter = w.w
 | |
| 	} else {
 | |
| 		subWriter = w.partWriter
 | |
| 	}
 | |
| 
 | |
| 	if enc == Base64 {
 | |
| 		wc := base64.NewEncoder(base64.StdEncoding, newBase64LineWriter(subWriter))
 | |
| 		w.err = f(wc)
 | |
| 		wc.Close()
 | |
| 	} else if enc == Unencoded {
 | |
| 		w.err = f(subWriter)
 | |
| 	} else {
 | |
| 		wc := newQPWriter(subWriter)
 | |
| 		w.err = f(wc)
 | |
| 		wc.Close()
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // As required by RFC 2045, 6.7. (page 21) for quoted-printable, and
 | |
| // RFC 2045, 6.8. (page 25) for base64.
 | |
| const maxLineLen = 76
 | |
| 
 | |
| // base64LineWriter limits text encoded in base64 to 76 characters per line
 | |
| type base64LineWriter struct {
 | |
| 	w       io.Writer
 | |
| 	lineLen int
 | |
| }
 | |
| 
 | |
| func newBase64LineWriter(w io.Writer) *base64LineWriter {
 | |
| 	return &base64LineWriter{w: w}
 | |
| }
 | |
| 
 | |
| func (w *base64LineWriter) Write(p []byte) (int, error) {
 | |
| 	n := 0
 | |
| 	for len(p)+w.lineLen > maxLineLen {
 | |
| 		w.w.Write(p[:maxLineLen-w.lineLen])
 | |
| 		w.w.Write([]byte("\r\n"))
 | |
| 		p = p[maxLineLen-w.lineLen:]
 | |
| 		n += maxLineLen - w.lineLen
 | |
| 		w.lineLen = 0
 | |
| 	}
 | |
| 
 | |
| 	w.w.Write(p)
 | |
| 	w.lineLen += len(p)
 | |
| 
 | |
| 	return n + len(p), nil
 | |
| }
 | |
| 
 | |
| // Stubbed out for testing.
 | |
| var now = time.Now
 | |
| 
 |