// SPDX-FileCopyrightText: 2022-2023 The go-mail Authors // // SPDX-License-Identifier: MIT package mail import ( "bytes" "encoding/base64" "fmt" "io" "mime" "mime/multipart" "mime/quotedprintable" "net/textproto" "sort" "strings" ) // MaxHeaderLength defines the maximum line length for a mail header // RFC 2047 suggests 76 characters const MaxHeaderLength = 76 // MaxBodyLength defines the maximum line length for the mail body // RFC 2047 suggests 76 characters const MaxBodyLength = 76 // SingleNewLine represents a new line that can be used by the msgWriter to issue a carriage return const SingleNewLine = "\r\n" // DoubleNewLine represents a double new line that can be used by the msgWriter to // indicate a new segment of the mail const DoubleNewLine = "\r\n\r\n" // msgWriter handles the I/O to the io.WriteCloser of the SMTP client type msgWriter struct { c Charset d int8 en mime.WordEncoder err error mpw [3]*multipart.Writer n int64 pw io.Writer w io.Writer } // Write implements the io.Writer interface for msgWriter func (mw *msgWriter) Write(p []byte) (int, error) { if mw.err != nil { return 0, fmt.Errorf("failed to write due to previous error: %w", mw.err) } var n int n, mw.err = mw.w.Write(p) mw.n += int64(n) return n, mw.err } // writeMsg formats the message and sends it to its io.Writer func (mw *msgWriter) writeMsg(m *Msg) { m.addDefaultHeader() mw.writeGenHeader(m) mw.writePreformattedGenHeader(m) // Set the FROM header (or envelope FROM if FROM is empty) hf := true f, ok := m.addrHeader[HeaderFrom] if !ok || (len(f) == 0 || f == nil) { f, ok = m.addrHeader[HeaderEnvelopeFrom] if !ok || (len(f) == 0 || f == nil) { hf = false } } if hf && (len(f) > 0 && f[0] != nil) { mw.writeHeader(Header(HeaderFrom), f[0].String()) } // Set the rest of the address headers for _, t := range []AddrHeader{HeaderTo, HeaderCc} { if al, ok := m.addrHeader[t]; ok { var v []string for _, a := range al { v = append(v, a.String()) } mw.writeHeader(Header(t), v...) } } if m.hasAlt() { mw.startMP(MIMEAlternative, m.boundary) mw.writeString(DoubleNewLine) } for _, p := range m.parts { if !p.del { mw.writePart(p, m.charset) } } if m.hasAlt() { mw.stopMP() } } // writeGenHeader writes out all generic headers to the msgWriter func (mw *msgWriter) writeGenHeader(m *Msg) { gk := make([]string, 0, len(m.genHeader)) for k := range m.genHeader { gk = append(gk, string(k)) } sort.Strings(gk) for _, k := range gk { mw.writeHeader(Header(k), m.genHeader[Header(k)]...) } } // writePreformattedHeader writes out all preformatted generic headers to the msgWriter func (mw *msgWriter) writePreformattedGenHeader(m *Msg) { for k, v := range m.preformHeader { mw.writeString(fmt.Sprintf("%s: %s%s", k, v, SingleNewLine)) } } // startMP writes a multipart beginning func (mw *msgWriter) startMP(mt MIMEType, b string) { mp := multipart.NewWriter(mw) if b != "" { mw.err = mp.SetBoundary(b) } ct := fmt.Sprintf("multipart/%s;\r\n boundary=%s", mt, mp.Boundary()) mw.mpw[mw.d] = mp if mw.d == 0 { mw.writeString(fmt.Sprintf("%s: %s", HeaderContentType, ct)) } if mw.d > 0 { mw.newPart(map[string][]string{"Content-Type": {ct}}) } mw.d++ } // stopMP closes the multipart func (mw *msgWriter) stopMP() { if mw.d > 0 { mw.err = mw.mpw[mw.d-1].Close() mw.d-- } } // newPart creates a new MIME multipart io.Writer and sets the part writer to it func (mw *msgWriter) newPart(h map[string][]string) { mw.pw, mw.err = mw.mpw[mw.d-1].CreatePart(h) } // writePart writes the corresponding part to the Msg body func (mw *msgWriter) writePart(p *Part, cs Charset) { pcs := p.cset if pcs.String() == "" { pcs = cs } ct := fmt.Sprintf("%s; charset=%s", p.ctype, pcs) cte := p.enc.String() if mw.d == 0 { mw.writeHeader(HeaderContentType, ct) mw.writeHeader(HeaderContentTransferEnc, cte) mw.writeString(SingleNewLine) } if mw.d > 0 { mh := textproto.MIMEHeader{} mh.Add(string(HeaderContentType), ct) mh.Add(string(HeaderContentTransferEnc), cte) mw.newPart(mh) } mw.writeBody(p.w, p.enc) } // writeString writes a string into the msgWriter's io.Writer interface func (mw *msgWriter) writeString(s string) { if mw.err != nil { return } var n int n, mw.err = io.WriteString(mw.w, s) mw.n += int64(n) } // writeHeader writes a header into the msgWriter's io.Writer func (mw *msgWriter) writeHeader(k Header, vl ...string) { wbuf := bytes.Buffer{} cl := MaxHeaderLength - 2 wbuf.WriteString(string(k)) cl -= len(k) if len(vl) == 0 { wbuf.WriteString(":\r\n") return } wbuf.WriteString(": ") cl -= 2 fs := strings.Join(vl, ", ") sfs := strings.Split(fs, " ") for i, v := range sfs { if cl-len(v) <= 1 { wbuf.WriteString(fmt.Sprintf("%s ", SingleNewLine)) cl = MaxHeaderLength - 3 } wbuf.WriteString(v) if i < len(sfs)-1 { wbuf.WriteString(" ") cl -= 1 } cl -= len(v) } bufs := wbuf.String() bufs = strings.ReplaceAll(bufs, fmt.Sprintf(" %s", SingleNewLine), SingleNewLine) mw.writeString(bufs) mw.writeString("\r\n") } // writeBody writes an io.Reader into an io.Writer using provided Encoding func (mw *msgWriter) writeBody(f func(io.Writer) (int64, error), e Encoding) { var w io.Writer var ew io.WriteCloser var n int64 var err error if mw.d == 0 { w = mw.w } if mw.d > 0 { w = mw.pw } wbuf := bytes.Buffer{} lb := Base64LineBreaker{} lb.out = &wbuf switch e { case EncodingQP: ew = quotedprintable.NewWriter(&wbuf) case EncodingB64: ew = base64.NewEncoder(base64.StdEncoding, &lb) case NoEncoding: _, err = f(&wbuf) if err != nil { mw.err = fmt.Errorf("bodyWriter function: %w", err) } n, err = io.Copy(w, &wbuf) if err != nil && mw.err == nil { mw.err = fmt.Errorf("bodyWriter io.Copy: %w", err) } if mw.d == 0 { mw.n += n } return default: ew = quotedprintable.NewWriter(w) } _, err = f(ew) if err != nil { mw.err = fmt.Errorf("bodyWriter function: %w", err) } err = ew.Close() if err != nil && mw.err == nil { mw.err = fmt.Errorf("bodyWriter close encoded writer: %w", err) } err = lb.Close() if err != nil && mw.err == nil { mw.err = fmt.Errorf("bodyWriter close linebreaker: %w", err) } n, err = io.Copy(w, &wbuf) if err != nil && mw.err == nil { mw.err = fmt.Errorf("bodyWriter io.Copy: %w", err) } // Since the part writer uses the WriteTo() method, we don't need to add the // bytes twice if mw.d == 0 { mw.n += n } }