1package mail 2 3import ( 4 "bytes" 5 "io" 6 "os" 7 "path/filepath" 8 "time" 9) 10 11// Message represents an email. 12type Message struct { 13 header header 14 parts []*part 15 attachments []*file 16 embedded []*file 17 charset string 18 encoding Encoding 19 hEncoder mimeEncoder 20 buf bytes.Buffer 21 boundary string 22} 23 24type header map[string][]string 25 26type part struct { 27 contentType string 28 copier func(io.Writer) error 29 encoding Encoding 30} 31 32// NewMessage creates a new message. It uses UTF-8 and quoted-printable encoding 33// by default. 34func NewMessage(settings ...MessageSetting) *Message { 35 m := &Message{ 36 header: make(header), 37 charset: "UTF-8", 38 encoding: QuotedPrintable, 39 } 40 41 m.applySettings(settings) 42 43 if m.encoding == Base64 { 44 m.hEncoder = bEncoding 45 } else { 46 m.hEncoder = qEncoding 47 } 48 49 return m 50} 51 52// Reset resets the message so it can be reused. The message keeps its previous 53// settings so it is in the same state that after a call to NewMessage. 54func (m *Message) Reset() { 55 for k := range m.header { 56 delete(m.header, k) 57 } 58 m.parts = nil 59 m.attachments = nil 60 m.embedded = nil 61} 62 63func (m *Message) applySettings(settings []MessageSetting) { 64 for _, s := range settings { 65 s(m) 66 } 67} 68 69// A MessageSetting can be used as an argument in NewMessage to configure an 70// email. 71type MessageSetting func(m *Message) 72 73// SetCharset is a message setting to set the charset of the email. 74func SetCharset(charset string) MessageSetting { 75 return func(m *Message) { 76 m.charset = charset 77 } 78} 79 80// SetEncoding is a message setting to set the encoding of the email. 81func SetEncoding(enc Encoding) MessageSetting { 82 return func(m *Message) { 83 m.encoding = enc 84 } 85} 86 87// Encoding represents a MIME encoding scheme like quoted-printable or base64. 88type Encoding string 89 90const ( 91 // QuotedPrintable represents the quoted-printable encoding as defined in 92 // RFC 2045. 93 QuotedPrintable Encoding = "quoted-printable" 94 // Base64 represents the base64 encoding as defined in RFC 2045. 95 Base64 Encoding = "base64" 96 // Unencoded can be used to avoid encoding the body of an email. The headers 97 // will still be encoded using quoted-printable encoding. 98 Unencoded Encoding = "8bit" 99) 100 101// SetBoundary sets a custom multipart boundary. 102func (m *Message) SetBoundary(boundary string) { 103 m.boundary = boundary 104} 105 106// SetHeader sets a value to the given header field. 107func (m *Message) SetHeader(field string, value ...string) { 108 m.encodeHeader(value) 109 m.header[field] = value 110} 111 112func (m *Message) encodeHeader(values []string) { 113 for i := range values { 114 values[i] = m.encodeString(values[i]) 115 } 116} 117 118func (m *Message) encodeString(value string) string { 119 return m.hEncoder.Encode(m.charset, value) 120} 121 122// SetHeaders sets the message headers. 123func (m *Message) SetHeaders(h map[string][]string) { 124 for k, v := range h { 125 m.SetHeader(k, v...) 126 } 127} 128 129// SetAddressHeader sets an address to the given header field. 130func (m *Message) SetAddressHeader(field, address, name string) { 131 m.header[field] = []string{m.FormatAddress(address, name)} 132} 133 134// FormatAddress formats an address and a name as a valid RFC 5322 address. 135func (m *Message) FormatAddress(address, name string) string { 136 if name == "" { 137 return address 138 } 139 140 enc := m.encodeString(name) 141 if enc == name { 142 m.buf.WriteByte('"') 143 for i := 0; i < len(name); i++ { 144 b := name[i] 145 if b == '\\' || b == '"' { 146 m.buf.WriteByte('\\') 147 } 148 m.buf.WriteByte(b) 149 } 150 m.buf.WriteByte('"') 151 } else if hasSpecials(name) { 152 m.buf.WriteString(bEncoding.Encode(m.charset, name)) 153 } else { 154 m.buf.WriteString(enc) 155 } 156 m.buf.WriteString(" <") 157 m.buf.WriteString(address) 158 m.buf.WriteByte('>') 159 160 addr := m.buf.String() 161 m.buf.Reset() 162 return addr 163} 164 165func hasSpecials(text string) bool { 166 for i := 0; i < len(text); i++ { 167 switch c := text[i]; c { 168 case '(', ')', '<', '>', '[', ']', ':', ';', '@', '\\', ',', '.', '"': 169 return true 170 } 171 } 172 173 return false 174} 175 176// SetDateHeader sets a date to the given header field. 177func (m *Message) SetDateHeader(field string, date time.Time) { 178 m.header[field] = []string{m.FormatDate(date)} 179} 180 181// FormatDate formats a date as a valid RFC 5322 date. 182func (m *Message) FormatDate(date time.Time) string { 183 return date.Format(time.RFC1123Z) 184} 185 186// GetHeader gets a header field. 187func (m *Message) GetHeader(field string) []string { 188 return m.header[field] 189} 190 191// SetBody sets the body of the message. It replaces any content previously set 192// by SetBody, SetBodyWriter, AddAlternative or AddAlternativeWriter. 193func (m *Message) SetBody(contentType, body string, settings ...PartSetting) { 194 m.SetBodyWriter(contentType, newCopier(body), settings...) 195} 196 197// SetBodyWriter sets the body of the message. It can be useful with the 198// text/template or html/template packages. 199func (m *Message) SetBodyWriter(contentType string, f func(io.Writer) error, settings ...PartSetting) { 200 m.parts = []*part{m.newPart(contentType, f, settings)} 201} 202 203// AddAlternative adds an alternative part to the message. 204// 205// It is commonly used to send HTML emails that default to the plain text 206// version for backward compatibility. AddAlternative appends the new part to 207// the end of the message. So the plain text part should be added before the 208// HTML part. See http://en.wikipedia.org/wiki/MIME#Alternative 209func (m *Message) AddAlternative(contentType, body string, settings ...PartSetting) { 210 m.AddAlternativeWriter(contentType, newCopier(body), settings...) 211} 212 213func newCopier(s string) func(io.Writer) error { 214 return func(w io.Writer) error { 215 _, err := io.WriteString(w, s) 216 return err 217 } 218} 219 220// AddAlternativeWriter adds an alternative part to the message. It can be 221// useful with the text/template or html/template packages. 222func (m *Message) AddAlternativeWriter(contentType string, f func(io.Writer) error, settings ...PartSetting) { 223 m.parts = append(m.parts, m.newPart(contentType, f, settings)) 224} 225 226func (m *Message) newPart(contentType string, f func(io.Writer) error, settings []PartSetting) *part { 227 p := &part{ 228 contentType: contentType, 229 copier: f, 230 encoding: m.encoding, 231 } 232 233 for _, s := range settings { 234 s(p) 235 } 236 237 return p 238} 239 240// A PartSetting can be used as an argument in Message.SetBody, 241// Message.SetBodyWriter, Message.AddAlternative or Message.AddAlternativeWriter 242// to configure the part added to a message. 243type PartSetting func(*part) 244 245// SetPartEncoding sets the encoding of the part added to the message. By 246// default, parts use the same encoding than the message. 247func SetPartEncoding(e Encoding) PartSetting { 248 return PartSetting(func(p *part) { 249 p.encoding = e 250 }) 251} 252 253type file struct { 254 Name string 255 Header map[string][]string 256 CopyFunc func(w io.Writer) error 257} 258 259func (f *file) setHeader(field, value string) { 260 f.Header[field] = []string{value} 261} 262 263// A FileSetting can be used as an argument in Message.Attach or Message.Embed. 264type FileSetting func(*file) 265 266// SetHeader is a file setting to set the MIME header of the message part that 267// contains the file content. 268// 269// Mandatory headers are automatically added if they are not set when sending 270// the email. 271func SetHeader(h map[string][]string) FileSetting { 272 return func(f *file) { 273 for k, v := range h { 274 f.Header[k] = v 275 } 276 } 277} 278 279// Rename is a file setting to set the name of the attachment if the name is 280// different than the filename on disk. 281func Rename(name string) FileSetting { 282 return func(f *file) { 283 f.Name = name 284 } 285} 286 287// SetCopyFunc is a file setting to replace the function that runs when the 288// message is sent. It should copy the content of the file to the io.Writer. 289// 290// The default copy function opens the file with the given filename, and copy 291// its content to the io.Writer. 292func SetCopyFunc(f func(io.Writer) error) FileSetting { 293 return func(fi *file) { 294 fi.CopyFunc = f 295 } 296} 297 298// AttachReader attaches a file using an io.Reader 299func (m *Message) AttachReader(name string, r io.Reader, settings ...FileSetting) { 300 m.attachments = m.appendFile(m.attachments, fileFromReader(name, r), settings) 301} 302 303// Attach attaches the files to the email. 304func (m *Message) Attach(filename string, settings ...FileSetting) { 305 m.attachments = m.appendFile(m.attachments, fileFromFilename(filename), settings) 306} 307 308// EmbedReader embeds the images to the email. 309func (m *Message) EmbedReader(name string, r io.Reader, settings ...FileSetting) { 310 m.embedded = m.appendFile(m.embedded, fileFromReader(name, r), settings) 311} 312 313// Embed embeds the images to the email. 314func (m *Message) Embed(filename string, settings ...FileSetting) { 315 m.embedded = m.appendFile(m.embedded, fileFromFilename(filename), settings) 316} 317 318func fileFromFilename(name string) *file { 319 return &file{ 320 Name: filepath.Base(name), 321 Header: make(map[string][]string), 322 CopyFunc: func(w io.Writer) error { 323 h, err := os.Open(name) 324 if err != nil { 325 return err 326 } 327 if _, err := io.Copy(w, h); err != nil { 328 h.Close() 329 return err 330 } 331 return h.Close() 332 }, 333 } 334} 335 336func fileFromReader(name string, r io.Reader) *file { 337 return &file{ 338 Name: filepath.Base(name), 339 Header: make(map[string][]string), 340 CopyFunc: func(w io.Writer) error { 341 if _, err := io.Copy(w, r); err != nil { 342 return err 343 } 344 return nil 345 }, 346 } 347} 348 349func (m *Message) appendFile(list []*file, f *file, settings []FileSetting) []*file { 350 for _, s := range settings { 351 s(f) 352 } 353 354 if list == nil { 355 return []*file{f} 356 } 357 358 return append(list, f) 359} 360