1package message
2
3import (
4 "io"
5)
6
7// Writer is a write-through helper, collecting properties about the written
8// message and replacing bare \n line endings with \r\n.
9type Writer struct {
10 writer io.Writer
11
12 HaveBody bool // Body is optional in a message. ../rfc/5322:343
13 Has8bit bool // Whether a byte with the high/8bit has been read. So whether this needs SMTP 8BITMIME instead of 7BIT.
14 Size int64 // Number of bytes written, may be different from bytes read due to LF to CRLF conversion.
15
16 // For detecting header/body-separating crlf and fixing up bare lf. These are the
17 // incoming bytes, not the fixed up bytes. So CRs may be missing from tail.
18 tail [3]byte
19 // todo: should be parsing headers here, as we go
20}
21
22func NewWriter(w io.Writer) *Writer {
23 // Pretend we already saw \r\n, for handling empty header.
24 return &Writer{writer: w, tail: [3]byte{0, '\r', '\n'}}
25}
26
27// Write implements io.Writer, and writes buf as message to the Writer's underlying
28// io.Writer. It converts bare new lines (LF) to carriage returns with new lines
29// (CRLF).
30func (w *Writer) Write(buf []byte) (int, error) {
31 if !w.Has8bit {
32 for _, b := range buf {
33 if b >= 0x80 {
34 w.Has8bit = true
35 break
36 }
37 }
38 }
39
40 if !w.HaveBody {
41 get := func(i int) byte {
42 if i < 0 {
43 return w.tail[3+i]
44 }
45 return buf[i]
46 }
47
48 for i, b := range buf {
49 if b == '\n' && (get(i-1) == '\n' || get(i-1) == '\r' && get(i-2) == '\n') {
50 w.HaveBody = true
51 break
52 }
53 }
54 }
55
56 // Update w.tail after having written. Regardless of error, writers can't expect
57 // subsequent writes to work again properly anyway.
58 defer func() {
59 n := len(buf)
60 if n > 3 {
61 n = 3
62 }
63 copy(w.tail[:], w.tail[n:])
64 copy(w.tail[3-n:], buf[len(buf)-n:])
65 }()
66
67 wrote := 0
68 o := 0
69Top:
70 for o < len(buf) {
71 // Look for bare newline. If present, write up to that position while adding the
72 // missing carriage return. Then start the loop again.
73 for i := o; i < len(buf); i++ {
74 if buf[i] == '\n' && (i > 0 && buf[i-1] != '\r' || i == 0 && w.tail[2] != '\r') {
75 // Write buffer leading up to missing \r.
76 if i > o {
77 n, err := w.writer.Write(buf[o:i])
78 if n > 0 {
79 wrote += n
80 w.Size += int64(n)
81 }
82 if err != nil {
83 return wrote, err
84 }
85 }
86 n, err := w.writer.Write([]byte{'\r', '\n'})
87 if n == 2 {
88 wrote += 1 // For only the newline.
89 w.Size += int64(2)
90 }
91 if err != nil {
92 return wrote, err
93 }
94 o = i + 1
95 continue Top
96 }
97 }
98 n, err := w.writer.Write(buf[o:])
99 if n > 0 {
100 wrote += n
101 w.Size += int64(n)
102 }
103 if err != nil {
104 return wrote, err
105 }
106 break
107 }
108 return wrote, nil
109}
110