13	"github.com/mjl-/mox/smtp"
 
17	ErrMessageSize = errors.New("message too large")
 
18	ErrCompose     = errors.New("compose")
 
21// Composer helps compose a message. Operations that fail call panic, which should
 
22// be caught with recover(), checking for ErrCompose and optionally ErrMessageSize.
 
23// Writes are buffered.
 
25	Has8bit  bool  // Whether message contains 8bit data.
 
26	SMTPUTF8 bool  // Whether message needs to be sent with SMTPUTF8 extension.
 
27	Size     int64 // Total bytes written.
 
30	maxSize int64 // If greater than zero, writes beyond maximum size raise ErrMessageSize.
 
33// NewComposer initializes a new composer with a buffered writer around w, and
 
34// with a maximum message size if maxSize is greater than zero.
 
36// smtputf8 must be set when the message must be delivered with smtputf8: if any
 
37// email address localpart has non-ascii (utf-8).
 
39// Operations on a Composer do not return an error. Caller must use recover() to
 
40// catch ErrCompose and optionally ErrMessageSize errors.
 
41func NewComposer(w io.Writer, maxSize int64, smtputf8 bool) *Composer {
 
42	return &Composer{bw: bufio.NewWriter(w), maxSize: maxSize, SMTPUTF8: smtputf8, Has8bit: smtputf8}
 
45// Write implements io.Writer, but calls panic (that is handled higher up) on
 
47func (c *Composer) Write(buf []byte) (int, error) {
 
48	if c.maxSize > 0 && c.Size+int64(len(buf)) > c.maxSize {
 
49		c.Checkf(ErrMessageSize, "writing message")
 
51	n, err := c.bw.Write(buf)
 
55	c.Checkf(err, "write")
 
59// Checkf checks err, panicing with sentinel error value.
 
60func (c *Composer) Checkf(err error, format string, args ...any) {
 
62		// We expose the original error too, needed at least for ErrMessageSize.
 
63		panic(fmt.Errorf("%w: %w: %v", ErrCompose, err, fmt.Sprintf(format, args...)))
 
67// Flush writes any buffered output.
 
68func (c *Composer) Flush() {
 
70	c.Checkf(err, "flush")
 
73// Header writes a message header.
 
74func (c *Composer) Header(k, v string) {
 
75	fmt.Fprintf(c, "%s: %s\r\n", k, v)
 
78// NameAddress holds both an address display name, and an SMTP path address.
 
79type NameAddress struct {
 
84// HeaderAddrs writes a message header with addresses.
 
85func (c *Composer) HeaderAddrs(k string, l []NameAddress) {
 
90	linelen := len(k) + len(": ")
 
96		addr := mail.Address{Name: a.DisplayName, Address: a.Address.Pack(c.SMTPUTF8)}
 
98		if v != "" && linelen+1+len(s) > 77 {
 
108	fmt.Fprintf(c, "%s: %s\r\n", k, v)
 
111// Subject writes a subject message header.
 
112func (c *Composer) Subject(subject string) {
 
113	var subjectValue string
 
114	subjectLineLen := len("Subject: ")
 
116	for i, word := range strings.Split(subject, " ") {
 
117		if !c.SMTPUTF8 && !isASCII(word) {
 
118			word = mime.QEncoding.Encode("utf-8", word)
 
124		if subjectWord && subjectLineLen+len(word) > 77 {
 
125			subjectValue += "\r\n\t"
 
129		subjectLineLen += len(word)
 
132	c.Header("Subject", subjectValue)
 
135// Line writes an empty line.
 
136func (c *Composer) Line() {
 
137	_, _ = c.Write([]byte("\r\n"))
 
140// TextPart prepares a text part to be added. Text should contain lines terminated
 
141// with newlines (lf), which are replaced with crlf. The returned text may be
 
142// quotedprintable, if needed. The returned ct and cte headers are for use with
 
143// Content-Type and Content-Transfer-Encoding headers.
 
144func (c *Composer) TextPart(subtype, text string) (textBody []byte, ct, cte string) {
 
145	if !strings.HasSuffix(text, "\n") {
 
148	text = strings.ReplaceAll(text, "\n", "\r\n")
 
149	charset := "us-ascii"
 
153	if NeedsQuotedPrintable(text) {
 
154		var sb strings.Builder
 
155		_, err := io.Copy(quotedprintable.NewWriter(&sb), strings.NewReader(text))
 
156		c.Checkf(err, "converting text to quoted printable")
 
158		cte = "quoted-printable"
 
159	} else if c.Has8bit || charset == "utf-8" {
 
165	ct = mime.FormatMediaType("text/"+subtype, map[string]string{"charset": charset})
 
166	return []byte(text), ct, cte
 
169func isASCII(s string) bool {
 
170	for _, c := range s {