1package message_test
2
3import (
4 "bytes"
5 "errors"
6 "fmt"
7 "io"
8 "log"
9 "log/slog"
10 "strings"
11 "time"
12
13 "github.com/mjl-/mox/dns"
14 "github.com/mjl-/mox/message"
15 "github.com/mjl-/mox/smtp"
16)
17
18func ExampleDecodeReader() {
19 // Convert from iso-8859-1 to utf-8.
20 input := []byte{'t', 0xe9, 's', 't'}
21 output, err := io.ReadAll(message.DecodeReader("iso-8859-1", bytes.NewReader(input)))
22 if err != nil {
23 log.Fatalf("read from decoder: %v", err)
24 }
25 fmt.Printf("%s\n", string(output))
26 // Output: tést
27}
28
29func ExampleMessageIDCanonical() {
30 // Valid message-id.
31 msgid, invalidAddress, err := message.MessageIDCanonical("<ok@localhost>")
32 if err != nil {
33 fmt.Printf("invalid message-id: %v\n", err)
34 } else {
35 fmt.Printf("canonical: %s %v\n", msgid, invalidAddress)
36 }
37
38 // Missing <>.
39 msgid, invalidAddress, err = message.MessageIDCanonical("bogus@localhost")
40 if err != nil {
41 fmt.Printf("invalid message-id: %v\n", err)
42 } else {
43 fmt.Printf("canonical: %s %v\n", msgid, invalidAddress)
44 }
45
46 // Invalid address, but returned as not being in error.
47 msgid, invalidAddress, err = message.MessageIDCanonical("<invalid>")
48 if err != nil {
49 fmt.Printf("invalid message-id: %v\n", err)
50 } else {
51 fmt.Printf("canonical: %s %v\n", msgid, invalidAddress)
52 }
53
54 // Output:
55 // canonical: ok@localhost false
56 // invalid message-id: not a message-id: missing <
57 // canonical: invalid true
58}
59
60func ExampleThreadSubject() {
61 // Basic subject.
62 s, isResp := message.ThreadSubject("nothing special", false)
63 fmt.Printf("%s, response: %v\n", s, isResp)
64
65 // List tags and "re:" are stripped.
66 s, isResp = message.ThreadSubject("[list1] [list2] Re: test", false)
67 fmt.Printf("%s, response: %v\n", s, isResp)
68
69 // "fwd:" is stripped.
70 s, isResp = message.ThreadSubject("fwd: a forward", false)
71 fmt.Printf("%s, response: %v\n", s, isResp)
72
73 // Trailing "(fwd)" is also a forward.
74 s, isResp = message.ThreadSubject("another forward (fwd)", false)
75 fmt.Printf("%s, response: %v\n", s, isResp)
76
77 // [fwd: ...] is stripped.
78 s, isResp = message.ThreadSubject("[fwd: [list] fwd: re: it's complicated]", false)
79 fmt.Printf("%s, response: %v\n", s, isResp)
80
81 // Output:
82 // nothing special, response: false
83 // test, response: true
84 // a forward, response: true
85 // another forward, response: true
86 // it's complicated, response: true
87}
88
89func ExampleComposer() {
90 // We store in a buffer. We could also write to a file.
91 var b bytes.Buffer
92
93 // NewComposer. Keep in mind that operations on a Composer will panic on error.
94 const smtputf8 = false
95 xc := message.NewComposer(&b, 10*1024*1024, smtputf8)
96
97 // Catch and handle errors when composing.
98 defer func() {
99 x := recover()
100 if x == nil {
101 return
102 }
103 if err, ok := x.(error); ok && errors.Is(err, message.ErrCompose) {
104 log.Printf("compose: %v", err)
105 }
106 panic(x)
107 }()
108
109 // Add an address header.
110 xc.HeaderAddrs("From", []message.NameAddress{{DisplayName: "Charlie", Address: smtp.NewAddress("root", dns.Domain{ASCII: "localhost"})}})
111
112 // Add subject header, with encoding
113 xc.Subject("hi ☺")
114
115 // Add Date and Message-ID headers, required.
116 tm, _ := time.Parse(time.RFC3339, "2006-01-02T15:04:05+07:00")
117 xc.Header("Date", tm.Format(message.RFC5322Z))
118 xc.Header("Message-ID", "<unique@host>") // Should generate unique id for each message.
119
120 xc.Header("MIME-Version", "1.0")
121
122 // Write content-* headers for the text body.
123 body, ct, cte := xc.TextPart("plain", "this is the body")
124 xc.Header("Content-Type", ct)
125 xc.Header("Content-Transfer-Encoding", cte)
126
127 // Header/Body separator
128 xc.Line()
129
130 // The part body. Use mime/multipart to make messages with multiple parts.
131 xc.Write(body)
132
133 // Flush any buffered writes to the original writer.
134 xc.Flush()
135
136 fmt.Println(strings.ReplaceAll(b.String(), "\r\n", "\n"))
137 // Output:
138 // From: "Charlie" <root@localhost>
139 // Subject: hi =?utf-8?q?=E2=98=BA?=
140 // Date: 2 Jan 2006 15:04:05 +0700
141 // Message-ID: <unique@host>
142 // MIME-Version: 1.0
143 // Content-Type: text/plain; charset=us-ascii
144 // Content-Transfer-Encoding: 7bit
145 //
146 // this is the body
147}
148
149func ExamplePart() {
150 // Parse a message from an io.ReaderAt, which could be a file.
151 strict := false
152 r := strings.NewReader("header: value\r\nanother: value\r\n\r\nbody ...\r\n")
153 part, err := message.Parse(slog.Default(), strict, r)
154 if err != nil {
155 log.Fatalf("parsing message: %v", err)
156 }
157
158 // The headers of the first part have been parsed, i.e. the message headers.
159 // A message can be multipart (e.g. alternative, related, mixed), and possibly
160 // nested.
161
162 // By walking the entire message, all part metadata (like offsets into the file
163 // where a part starts) is recorded.
164 err = part.Walk(slog.Default(), nil)
165 if err != nil {
166 log.Fatalf("walking message: %v", err)
167 }
168
169 // Messages can have a recursive multipart structure. Print the structure.
170 var printPart func(indent string, p message.Part)
171 printPart = func(indent string, p message.Part) {
172 log.Printf("%s- part: %v", indent, part)
173 for _, pp := range p.Parts {
174 printPart(" "+indent, pp)
175 }
176 }
177 printPart("", part)
178}
179
180func ExampleWriter() {
181 // NewWriter on a string builder.
182 var b strings.Builder
183 w := message.NewWriter(&b)
184
185 // Write some lines, some with proper CRLF line ending, others without.
186 fmt.Fprint(w, "header: value\r\n")
187 fmt.Fprint(w, "another: value\n") // missing \r
188 fmt.Fprint(w, "\r\n")
189 fmt.Fprint(w, "hi ☺\n") // missing \r
190
191 fmt.Printf("%q\n", b.String())
192 fmt.Printf("%v %v", w.HaveBody, w.Has8bit)
193 // Output:
194 // "header: value\r\nanother: value\r\n\r\nhi ☺\r\n"
195 // true true
196}
197