mirror of
https://codeberg.org/forgejo/forgejo.git
synced 2024-12-28 13:49:13 -05:00
323 lines
8 KiB
Go
323 lines
8 KiB
Go
|
package gomail
|
||
|
|
||
|
import (
|
||
|
"bytes"
|
||
|
"io"
|
||
|
"os"
|
||
|
"path/filepath"
|
||
|
"time"
|
||
|
)
|
||
|
|
||
|
// Message represents an email.
|
||
|
type Message struct {
|
||
|
header header
|
||
|
parts []*part
|
||
|
attachments []*file
|
||
|
embedded []*file
|
||
|
charset string
|
||
|
encoding Encoding
|
||
|
hEncoder mimeEncoder
|
||
|
buf bytes.Buffer
|
||
|
}
|
||
|
|
||
|
type header map[string][]string
|
||
|
|
||
|
type part struct {
|
||
|
contentType string
|
||
|
copier func(io.Writer) error
|
||
|
encoding Encoding
|
||
|
}
|
||
|
|
||
|
// NewMessage creates a new message. It uses UTF-8 and quoted-printable encoding
|
||
|
// by default.
|
||
|
func NewMessage(settings ...MessageSetting) *Message {
|
||
|
m := &Message{
|
||
|
header: make(header),
|
||
|
charset: "UTF-8",
|
||
|
encoding: QuotedPrintable,
|
||
|
}
|
||
|
|
||
|
m.applySettings(settings)
|
||
|
|
||
|
if m.encoding == Base64 {
|
||
|
m.hEncoder = bEncoding
|
||
|
} else {
|
||
|
m.hEncoder = qEncoding
|
||
|
}
|
||
|
|
||
|
return m
|
||
|
}
|
||
|
|
||
|
// Reset resets the message so it can be reused. The message keeps its previous
|
||
|
// settings so it is in the same state that after a call to NewMessage.
|
||
|
func (m *Message) Reset() {
|
||
|
for k := range m.header {
|
||
|
delete(m.header, k)
|
||
|
}
|
||
|
m.parts = nil
|
||
|
m.attachments = nil
|
||
|
m.embedded = nil
|
||
|
}
|
||
|
|
||
|
func (m *Message) applySettings(settings []MessageSetting) {
|
||
|
for _, s := range settings {
|
||
|
s(m)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// A MessageSetting can be used as an argument in NewMessage to configure an
|
||
|
// email.
|
||
|
type MessageSetting func(m *Message)
|
||
|
|
||
|
// SetCharset is a message setting to set the charset of the email.
|
||
|
func SetCharset(charset string) MessageSetting {
|
||
|
return func(m *Message) {
|
||
|
m.charset = charset
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// SetEncoding is a message setting to set the encoding of the email.
|
||
|
func SetEncoding(enc Encoding) MessageSetting {
|
||
|
return func(m *Message) {
|
||
|
m.encoding = enc
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Encoding represents a MIME encoding scheme like quoted-printable or base64.
|
||
|
type Encoding string
|
||
|
|
||
|
const (
|
||
|
// QuotedPrintable represents the quoted-printable encoding as defined in
|
||
|
// RFC 2045.
|
||
|
QuotedPrintable Encoding = "quoted-printable"
|
||
|
// Base64 represents the base64 encoding as defined in RFC 2045.
|
||
|
Base64 Encoding = "base64"
|
||
|
// Unencoded can be used to avoid encoding the body of an email. The headers
|
||
|
// will still be encoded using quoted-printable encoding.
|
||
|
Unencoded Encoding = "8bit"
|
||
|
)
|
||
|
|
||
|
// SetHeader sets a value to the given header field.
|
||
|
func (m *Message) SetHeader(field string, value ...string) {
|
||
|
m.encodeHeader(value)
|
||
|
m.header[field] = value
|
||
|
}
|
||
|
|
||
|
func (m *Message) encodeHeader(values []string) {
|
||
|
for i := range values {
|
||
|
values[i] = m.encodeString(values[i])
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (m *Message) encodeString(value string) string {
|
||
|
return m.hEncoder.Encode(m.charset, value)
|
||
|
}
|
||
|
|
||
|
// SetHeaders sets the message headers.
|
||
|
func (m *Message) SetHeaders(h map[string][]string) {
|
||
|
for k, v := range h {
|
||
|
m.SetHeader(k, v...)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// SetAddressHeader sets an address to the given header field.
|
||
|
func (m *Message) SetAddressHeader(field, address, name string) {
|
||
|
m.header[field] = []string{m.FormatAddress(address, name)}
|
||
|
}
|
||
|
|
||
|
// FormatAddress formats an address and a name as a valid RFC 5322 address.
|
||
|
func (m *Message) FormatAddress(address, name string) string {
|
||
|
if name == "" {
|
||
|
return address
|
||
|
}
|
||
|
|
||
|
enc := m.encodeString(name)
|
||
|
if enc == name {
|
||
|
m.buf.WriteByte('"')
|
||
|
for i := 0; i < len(name); i++ {
|
||
|
b := name[i]
|
||
|
if b == '\\' || b == '"' {
|
||
|
m.buf.WriteByte('\\')
|
||
|
}
|
||
|
m.buf.WriteByte(b)
|
||
|
}
|
||
|
m.buf.WriteByte('"')
|
||
|
} else if hasSpecials(name) {
|
||
|
m.buf.WriteString(bEncoding.Encode(m.charset, name))
|
||
|
} else {
|
||
|
m.buf.WriteString(enc)
|
||
|
}
|
||
|
m.buf.WriteString(" <")
|
||
|
m.buf.WriteString(address)
|
||
|
m.buf.WriteByte('>')
|
||
|
|
||
|
addr := m.buf.String()
|
||
|
m.buf.Reset()
|
||
|
return addr
|
||
|
}
|
||
|
|
||
|
func hasSpecials(text string) bool {
|
||
|
for i := 0; i < len(text); i++ {
|
||
|
switch c := text[i]; c {
|
||
|
case '(', ')', '<', '>', '[', ']', ':', ';', '@', '\\', ',', '.', '"':
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return false
|
||
|
}
|
||
|
|
||
|
// SetDateHeader sets a date to the given header field.
|
||
|
func (m *Message) SetDateHeader(field string, date time.Time) {
|
||
|
m.header[field] = []string{m.FormatDate(date)}
|
||
|
}
|
||
|
|
||
|
// FormatDate formats a date as a valid RFC 5322 date.
|
||
|
func (m *Message) FormatDate(date time.Time) string {
|
||
|
return date.Format(time.RFC1123Z)
|
||
|
}
|
||
|
|
||
|
// GetHeader gets a header field.
|
||
|
func (m *Message) GetHeader(field string) []string {
|
||
|
return m.header[field]
|
||
|
}
|
||
|
|
||
|
// SetBody sets the body of the message. It replaces any content previously set
|
||
|
// by SetBody, AddAlternative or AddAlternativeWriter.
|
||
|
func (m *Message) SetBody(contentType, body string, settings ...PartSetting) {
|
||
|
m.parts = []*part{m.newPart(contentType, newCopier(body), settings)}
|
||
|
}
|
||
|
|
||
|
// AddAlternative adds an alternative part to the message.
|
||
|
//
|
||
|
// It is commonly used to send HTML emails that default to the plain text
|
||
|
// version for backward compatibility. AddAlternative appends the new part to
|
||
|
// the end of the message. So the plain text part should be added before the
|
||
|
// HTML part. See http://en.wikipedia.org/wiki/MIME#Alternative
|
||
|
func (m *Message) AddAlternative(contentType, body string, settings ...PartSetting) {
|
||
|
m.AddAlternativeWriter(contentType, newCopier(body), settings...)
|
||
|
}
|
||
|
|
||
|
func newCopier(s string) func(io.Writer) error {
|
||
|
return func(w io.Writer) error {
|
||
|
_, err := io.WriteString(w, s)
|
||
|
return err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// AddAlternativeWriter adds an alternative part to the message. It can be
|
||
|
// useful with the text/template or html/template packages.
|
||
|
func (m *Message) AddAlternativeWriter(contentType string, f func(io.Writer) error, settings ...PartSetting) {
|
||
|
m.parts = append(m.parts, m.newPart(contentType, f, settings))
|
||
|
}
|
||
|
|
||
|
func (m *Message) newPart(contentType string, f func(io.Writer) error, settings []PartSetting) *part {
|
||
|
p := &part{
|
||
|
contentType: contentType,
|
||
|
copier: f,
|
||
|
encoding: m.encoding,
|
||
|
}
|
||
|
|
||
|
for _, s := range settings {
|
||
|
s(p)
|
||
|
}
|
||
|
|
||
|
return p
|
||
|
}
|
||
|
|
||
|
// A PartSetting can be used as an argument in Message.SetBody,
|
||
|
// Message.AddAlternative or Message.AddAlternativeWriter to configure the part
|
||
|
// added to a message.
|
||
|
type PartSetting func(*part)
|
||
|
|
||
|
// SetPartEncoding sets the encoding of the part added to the message. By
|
||
|
// default, parts use the same encoding than the message.
|
||
|
func SetPartEncoding(e Encoding) PartSetting {
|
||
|
return PartSetting(func(p *part) {
|
||
|
p.encoding = e
|
||
|
})
|
||
|
}
|
||
|
|
||
|
type file struct {
|
||
|
Name string
|
||
|
Header map[string][]string
|
||
|
CopyFunc func(w io.Writer) error
|
||
|
}
|
||
|
|
||
|
func (f *file) setHeader(field, value string) {
|
||
|
f.Header[field] = []string{value}
|
||
|
}
|
||
|
|
||
|
// A FileSetting can be used as an argument in Message.Attach or Message.Embed.
|
||
|
type FileSetting func(*file)
|
||
|
|
||
|
// SetHeader is a file setting to set the MIME header of the message part that
|
||
|
// contains the file content.
|
||
|
//
|
||
|
// Mandatory headers are automatically added if they are not set when sending
|
||
|
// the email.
|
||
|
func SetHeader(h map[string][]string) FileSetting {
|
||
|
return func(f *file) {
|
||
|
for k, v := range h {
|
||
|
f.Header[k] = v
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Rename is a file setting to set the name of the attachment if the name is
|
||
|
// different than the filename on disk.
|
||
|
func Rename(name string) FileSetting {
|
||
|
return func(f *file) {
|
||
|
f.Name = name
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// SetCopyFunc is a file setting to replace the function that runs when the
|
||
|
// message is sent. It should copy the content of the file to the io.Writer.
|
||
|
//
|
||
|
// The default copy function opens the file with the given filename, and copy
|
||
|
// its content to the io.Writer.
|
||
|
func SetCopyFunc(f func(io.Writer) error) FileSetting {
|
||
|
return func(fi *file) {
|
||
|
fi.CopyFunc = f
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func (m *Message) appendFile(list []*file, name string, settings []FileSetting) []*file {
|
||
|
f := &file{
|
||
|
Name: filepath.Base(name),
|
||
|
Header: make(map[string][]string),
|
||
|
CopyFunc: func(w io.Writer) error {
|
||
|
h, err := os.Open(name)
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if _, err := io.Copy(w, h); err != nil {
|
||
|
h.Close()
|
||
|
return err
|
||
|
}
|
||
|
return h.Close()
|
||
|
},
|
||
|
}
|
||
|
|
||
|
for _, s := range settings {
|
||
|
s(f)
|
||
|
}
|
||
|
|
||
|
if list == nil {
|
||
|
return []*file{f}
|
||
|
}
|
||
|
|
||
|
return append(list, f)
|
||
|
}
|
||
|
|
||
|
// Attach attaches the files to the email.
|
||
|
func (m *Message) Attach(filename string, settings ...FileSetting) {
|
||
|
m.attachments = m.appendFile(m.attachments, filename, settings)
|
||
|
}
|
||
|
|
||
|
// Embed embeds the images to the email.
|
||
|
func (m *Message) Embed(filename string, settings ...FileSetting) {
|
||
|
m.embedded = m.appendFile(m.embedded, filename, settings)
|
||
|
}
|