| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913 |
- package mail
- import (
- "bytes"
- "crypto/tls"
- "encoding/base64"
- "errors"
- "fmt"
- "io/ioutil"
- "mime"
- "net"
- "net/mail"
- "net/textproto"
- "path/filepath"
- "time"
- )
- // Email represents an email message.
- type Email struct {
- from string
- sender string
- replyTo string
- returnPath string
- recipients []string
- headers textproto.MIMEHeader
- parts []part
- attachments []*file
- inlines []*file
- Charset string
- Encoding encoding
- Error error
- SMTPServer *smtpClient
- }
- /*
- SMTPServer represents a SMTP Server
- If authentication is CRAM-MD5 then the Password is the Secret
- */
- type SMTPServer struct {
- Authentication authType
- Encryption encryption
- Username string
- Password string
- ConnectTimeout time.Duration
- SendTimeout time.Duration
- Host string
- Port int
- KeepAlive bool
- }
- //SMTPClient represents a SMTP Client for send email
- type SMTPClient struct {
- Client *smtpClient
- KeepAlive bool
- SendTimeout time.Duration
- }
- // part represents the different content parts of an email body.
- type part struct {
- contentType string
- body *bytes.Buffer
- }
- // file represents the files that can be added to the email message.
- type file struct {
- filename string
- mimeType string
- data []byte
- }
- type encryption int
- const (
- // EncryptionNone uses no encryption when sending email
- EncryptionNone encryption = iota
- // EncryptionSSL sets encryption type to SSL when sending email
- EncryptionSSL
- // EncryptionTLS sets encryption type to TLS when sending email
- EncryptionTLS
- )
- var encryptionTypes = [...]string{"None", "SSL", "TLS"}
- func (encryption encryption) string() string {
- return encryptionTypes[encryption]
- }
- type encoding int
- const (
- // EncodingNone turns off encoding on the message body
- EncodingNone encoding = iota
- // EncodingBase64 sets the message body encoding to base64
- EncodingBase64
- // EncodingQuotedPrintable sets the message body encoding to quoted-printable
- EncodingQuotedPrintable
- )
- var encodingTypes = [...]string{"binary", "base64", "quoted-printable"}
- func (encoding encoding) string() string {
- return encodingTypes[encoding]
- }
- type contentType int
- const (
- // TextPlain sets body type to text/plain in message body
- TextPlain contentType = iota
- // TextHTML sets body type to text/html in message body
- TextHTML
- )
- var contentTypes = [...]string{"text/plain", "text/html"}
- func (contentType contentType) string() string {
- return contentTypes[contentType]
- }
- type authType int
- const (
- // AuthPlain implements the PLAIN authentication
- AuthPlain authType = iota
- // AuthLogin implements the LOGIN authentication
- AuthLogin
- // AuthCRAMMD5 implements the CRAM-MD5 authentication
- AuthCRAMMD5
- )
- // NewMSG creates a new email. It uses UTF-8 by default. All charsets: http://webcheatsheet.com/HTML/character_sets_list.php
- func NewMSG() *Email {
- email := &Email{
- headers: make(textproto.MIMEHeader),
- Charset: "UTF-8",
- Encoding: EncodingQuotedPrintable,
- }
- email.AddHeader("MIME-Version", "1.0")
- return email
- }
- //NewSMTPClient returns the client for send email
- func NewSMTPClient() *SMTPServer {
- server := &SMTPServer{
- Authentication: AuthPlain,
- Encryption: EncryptionNone,
- ConnectTimeout: 10 * time.Second,
- SendTimeout: 10 * time.Second,
- }
- return server
- }
- // GetError returns the first email error encountered
- func (email *Email) GetError() error {
- return email.Error
- }
- // SetFrom sets the From address.
- func (email *Email) SetFrom(address string) *Email {
- if email.Error != nil {
- return email
- }
- email.AddAddresses("From", address)
- return email
- }
- // SetSender sets the Sender address.
- func (email *Email) SetSender(address string) *Email {
- if email.Error != nil {
- return email
- }
- email.AddAddresses("Sender", address)
- return email
- }
- // SetReplyTo sets the Reply-To address.
- func (email *Email) SetReplyTo(address string) *Email {
- if email.Error != nil {
- return email
- }
- email.AddAddresses("Reply-To", address)
- return email
- }
- // SetReturnPath sets the Return-Path address. This is most often used
- // to send bounced emails to a different email address.
- func (email *Email) SetReturnPath(address string) *Email {
- if email.Error != nil {
- return email
- }
- email.AddAddresses("Return-Path", address)
- return email
- }
- // AddTo adds a To address. You can provide multiple
- // addresses at the same time.
- func (email *Email) AddTo(addresses ...string) *Email {
- if email.Error != nil {
- return email
- }
- email.AddAddresses("To", addresses...)
- return email
- }
- // AddCc adds a Cc address. You can provide multiple
- // addresses at the same time.
- func (email *Email) AddCc(addresses ...string) *Email {
- if email.Error != nil {
- return email
- }
- email.AddAddresses("Cc", addresses...)
- return email
- }
- // AddBcc adds a Bcc address. You can provide multiple
- // addresses at the same time.
- func (email *Email) AddBcc(addresses ...string) *Email {
- if email.Error != nil {
- return email
- }
- email.AddAddresses("Bcc", addresses...)
- return email
- }
- // AddAddresses allows you to add addresses to the specified address header.
- func (email *Email) AddAddresses(header string, addresses ...string) *Email {
- if email.Error != nil {
- return email
- }
- found := false
- // check for a valid address header
- for _, h := range []string{"To", "Cc", "Bcc", "From", "Sender", "Reply-To", "Return-Path"} {
- if header == h {
- found = true
- }
- }
- if !found {
- email.Error = errors.New("Mail Error: Invalid address header; Header: [" + header + "]")
- return email
- }
- // check to see if the addresses are valid
- for i := range addresses {
- address, err := mail.ParseAddress(addresses[i])
- if err != nil {
- email.Error = errors.New("Mail Error: " + err.Error() + "; Header: [" + header + "] Address: [" + addresses[i] + "]")
- return email
- }
- // check for more than one address
- switch {
- case header == "From" && len(email.from) > 0:
- fallthrough
- case header == "Sender" && len(email.sender) > 0:
- fallthrough
- case header == "Reply-To" && len(email.replyTo) > 0:
- fallthrough
- case header == "Return-Path" && len(email.returnPath) > 0:
- email.Error = errors.New("Mail Error: There can only be one \"" + header + "\" address; Header: [" + header + "] Address: [" + addresses[i] + "]")
- return email
- default:
- // other address types can have more than one address
- }
- // save the address
- switch header {
- case "From":
- email.from = address.Address
- case "Sender":
- email.sender = address.Address
- case "Reply-To":
- email.replyTo = address.Address
- case "Return-Path":
- email.returnPath = address.Address
- default:
- // check that the address was added to the recipients list
- email.recipients, err = addAddress(email.recipients, address.Address)
- if err != nil {
- email.Error = errors.New("Mail Error: " + err.Error() + "; Header: [" + header + "] Address: [" + addresses[i] + "]")
- return email
- }
- }
- // make sure the from and sender addresses are different
- if email.from != "" && email.sender != "" && email.from == email.sender {
- email.sender = ""
- email.headers.Del("Sender")
- email.Error = errors.New("Mail Error: From and Sender should not be set to the same address")
- return email
- }
- // add all addresses to the headers except for Bcc and Return-Path
- if header != "Bcc" && header != "Return-Path" {
- // add the address to the headers
- email.headers.Add(header, address.String())
- }
- }
- return email
- }
- // addAddress adds an address to the address list if it hasn't already been added
- func addAddress(addressList []string, address string) ([]string, error) {
- // loop through the address list to check for dups
- for _, a := range addressList {
- if address == a {
- return addressList, errors.New("Mail Error: Address: [" + address + "] has already been added")
- }
- }
- return append(addressList, address), nil
- }
- type priority int
- const (
- // PriorityLow sets the email priority to Low
- PriorityLow priority = iota
- // PriorityHigh sets the email priority to High
- PriorityHigh
- )
- // SetPriority sets the email message priority. Use with
- // either "High" or "Low".
- func (email *Email) SetPriority(priority priority) *Email {
- if email.Error != nil {
- return email
- }
- switch priority {
- case PriorityLow:
- email.AddHeaders(textproto.MIMEHeader{
- "X-Priority": {"5 (Lowest)"},
- "X-MSMail-Priority": {"Low"},
- "Importance": {"Low"},
- })
- case PriorityHigh:
- email.AddHeaders(textproto.MIMEHeader{
- "X-Priority": {"1 (Highest)"},
- "X-MSMail-Priority": {"High"},
- "Importance": {"High"},
- })
- default:
- }
- return email
- }
- // SetDate sets the date header to the provided date/time.
- // The format of the string should be YYYY-MM-DD HH:MM:SS Time Zone.
- //
- // Example: SetDate("2015-04-28 10:32:00 CDT")
- func (email *Email) SetDate(dateTime string) *Email {
- if email.Error != nil {
- return email
- }
- const dateFormat = "2006-01-02 15:04:05 MST"
- // Try to parse the provided date/time
- dt, err := time.Parse(dateFormat, dateTime)
- if err != nil {
- email.Error = errors.New("Mail Error: Setting date failed with: " + err.Error())
- return email
- }
- email.headers.Set("Date", dt.Format(time.RFC1123Z))
- return email
- }
- // SetSubject sets the subject of the email message.
- func (email *Email) SetSubject(subject string) *Email {
- if email.Error != nil {
- return email
- }
- email.AddHeader("Subject", subject)
- return email
- }
- // SetBody sets the body of the email message.
- func (email *Email) SetBody(contentType contentType, body string) *Email {
- if email.Error != nil {
- return email
- }
- email.parts = []part{
- {
- contentType: contentType.string(),
- body: bytes.NewBufferString(body),
- },
- }
- return email
- }
- // AddHeader adds the given "header" with the passed "value".
- func (email *Email) AddHeader(header string, values ...string) *Email {
- if email.Error != nil {
- return email
- }
- // check that there is actually a value
- if len(values) < 1 {
- email.Error = errors.New("Mail Error: no value provided; Header: [" + header + "]")
- return email
- }
- switch header {
- case "Sender":
- fallthrough
- case "From":
- fallthrough
- case "To":
- fallthrough
- case "Bcc":
- fallthrough
- case "Cc":
- fallthrough
- case "Reply-To":
- fallthrough
- case "Return-Path":
- email.AddAddresses(header, values...)
- case "Date":
- if len(values) > 1 {
- email.Error = errors.New("Mail Error: To many dates provided")
- return email
- }
- email.SetDate(values[0])
- default:
- email.headers[header] = values
- }
- return email
- }
- // AddHeaders is used to add multiple headers at once
- func (email *Email) AddHeaders(headers textproto.MIMEHeader) *Email {
- if email.Error != nil {
- return email
- }
- for header, values := range headers {
- email.AddHeader(header, values...)
- }
- return email
- }
- // AddAlternative allows you to add alternative parts to the body
- // of the email message. This is most commonly used to add an
- // html version in addition to a plain text version that was
- // already added with SetBody.
- func (email *Email) AddAlternative(contentType contentType, body string) *Email {
- if email.Error != nil {
- return email
- }
- email.parts = append(email.parts,
- part{
- contentType: contentType.string(),
- body: bytes.NewBufferString(body),
- },
- )
- return email
- }
- // AddAttachment allows you to add an attachment to the email message.
- // You can optionally provide a different name for the file.
- func (email *Email) AddAttachment(file string, name ...string) *Email {
- if email.Error != nil {
- return email
- }
- if len(name) > 1 {
- email.Error = errors.New("Mail Error: Attach can only have a file and an optional name")
- return email
- }
- email.Error = email.attach(file, false, name...)
- return email
- }
- // AddAttachmentBase64 allows you to add an attachment in base64 to the email message.
- // You need provide a name for the file.
- func (email *Email) AddAttachmentBase64(b64File string, name string) *Email {
- if email.Error != nil {
- return email
- }
- if len(name) < 1 || len(b64File) < 1 {
- email.Error = errors.New("Mail Error: Attach Base64 need have a base64 string and name")
- return email
- }
- email.Error = email.attachB64(b64File, name)
- return email
- }
- // AddInline allows you to add an inline attachment to the email message.
- // You can optionally provide a different name for the file.
- func (email *Email) AddInline(file string, name ...string) *Email {
- if email.Error != nil {
- return email
- }
- if len(name) > 1 {
- email.Error = errors.New("Mail Error: Inline can only have a file and an optional name")
- return email
- }
- email.Error = email.attach(file, true, name...)
- return email
- }
- // attach does the low level attaching of the files
- func (email *Email) attach(f string, inline bool, name ...string) error {
- // Get the file data
- data, err := ioutil.ReadFile(f)
- if err != nil {
- return errors.New("Mail Error: Failed to add file with following error: " + err.Error())
- }
- // get the file mime type
- mimeType := mime.TypeByExtension(filepath.Ext(f))
- if mimeType == "" {
- mimeType = "application/octet-stream"
- }
- // get the filename
- _, filename := filepath.Split(f)
- // if an alternative filename was provided, use that instead
- if len(name) == 1 {
- filename = name[0]
- }
- if inline {
- email.inlines = append(email.inlines, &file{
- filename: filename,
- mimeType: mimeType,
- data: data,
- })
- } else {
- email.attachments = append(email.attachments, &file{
- filename: filename,
- mimeType: mimeType,
- data: data,
- })
- }
- return nil
- }
- // attachB64 does the low level attaching of the files but decoding base64 instead have a filepath
- func (email *Email) attachB64(b64File string, name string) error {
- // decode the string
- dec, err := base64.StdEncoding.DecodeString(b64File)
- if err != nil {
- return errors.New("Mail Error: Failed to decode base64 attachment with following error: " + err.Error())
- }
- // get the file mime type
- mimeType := mime.TypeByExtension(name)
- if mimeType == "" {
- mimeType = "application/octet-stream"
- }
- email.attachments = append(email.attachments, &file{
- filename: name,
- mimeType: mimeType,
- data: dec,
- })
- return nil
- }
- // getFrom returns the sender of the email, if any
- func (email *Email) getFrom() string {
- from := email.returnPath
- if from == "" {
- from = email.sender
- if from == "" {
- from = email.from
- if from == "" {
- from = email.replyTo
- }
- }
- }
- return from
- }
- func (email *Email) hasMixedPart() bool {
- return (len(email.parts) > 0 && len(email.attachments) > 0) || len(email.attachments) > 1
- }
- func (email *Email) hasRelatedPart() bool {
- return (len(email.parts) > 0 && len(email.inlines) > 0) || len(email.inlines) > 1
- }
- func (email *Email) hasAlternativePart() bool {
- return len(email.parts) > 1
- }
- // GetMessage builds and returns the email message
- func (email *Email) GetMessage() string {
- msg := newMessage(email)
- if email.hasMixedPart() {
- msg.openMultipart("mixed")
- }
- if email.hasRelatedPart() {
- msg.openMultipart("related")
- }
- if email.hasAlternativePart() {
- msg.openMultipart("alternative")
- }
- for _, part := range email.parts {
- msg.addBody(part.contentType, part.body.Bytes())
- }
- if email.hasAlternativePart() {
- msg.closeMultipart()
- }
- msg.addFiles(email.inlines, true)
- if email.hasRelatedPart() {
- msg.closeMultipart()
- }
- msg.addFiles(email.attachments, false)
- if email.hasMixedPart() {
- msg.closeMultipart()
- }
- return msg.getHeaders() + msg.body.String()
- }
- // Send sends the composed email
- func (email *Email) Send(client *SMTPClient) error {
- if email.Error != nil {
- return email.Error
- }
- if len(email.recipients) < 1 {
- return errors.New("Mail Error: No recipient specified")
- }
- msg := email.GetMessage()
- return send(email.from, email.recipients, msg, client)
- }
- // dial connects to the smtp server with the request encryption type
- func dial(host string, port string, encryption encryption, config *tls.Config) (*smtpClient, error) {
- var conn net.Conn
- var err error
- address := host + ":" + port
- // do the actual dial
- switch encryption {
- case EncryptionSSL:
- conn, err = tls.Dial("tcp", address, config)
- default:
- conn, err = net.Dial("tcp", address)
- }
- if err != nil {
- return nil, errors.New("Mail Error on dailing with encryption type " + encryption.string() + ": " + err.Error())
- }
- c, err := newClient(conn, host)
- if err != nil {
- return nil, errors.New("Mail Error on smtp dial: " + err.Error())
- }
- return c, err
- }
- // smtpConnect connects to the smtp server and starts TLS and passes auth
- // if necessary
- func smtpConnect(host string, port string, a auth, encryption encryption, config *tls.Config) (*smtpClient, error) {
- // connect to the mail server
- c, err := dial(host, port, encryption, config)
- if err != nil {
- return nil, err
- }
- // send Hello
- if err = c.hi("localhost"); err != nil {
- c.close()
- return nil, errors.New("Mail Error on Hello: " + err.Error())
- }
- // start TLS if necessary
- if encryption == EncryptionTLS {
- if ok, _ := c.extension("STARTTLS"); ok {
- if config.ServerName == "" {
- config = &tls.Config{ServerName: host}
- }
- if err = c.startTLS(config); err != nil {
- c.close()
- return nil, errors.New("Mail Error on Start TLS: " + err.Error())
- }
- }
- }
- // pass the authentication if necessary
- if a != nil {
- if ok, _ := c.extension("AUTH"); ok {
- if err = c.authenticate(a); err != nil {
- c.close()
- return nil, errors.New("Mail Error on Auth: " + err.Error())
- }
- }
- }
- return c, nil
- }
- //Connect returns the smtp client
- func (server *SMTPServer) Connect() (*SMTPClient, error) {
- var a auth
- switch server.Authentication {
- case AuthPlain:
- if server.Username != "" || server.Password != "" {
- a = plainAuthfn("", server.Username, server.Password, server.Host)
- }
- case AuthLogin:
- if server.Username != "" || server.Password != "" {
- a = loginAuthfn("", server.Username, server.Password, server.Host)
- }
- case AuthCRAMMD5:
- if server.Username != "" || server.Password != "" {
- a = cramMD5Authfn(server.Username, server.Password)
- }
- }
- var smtpConnectChannel chan error
- var c *smtpClient
- var err error
- // if there is a ConnectTimeout, setup the channel and do the connect under a goroutine
- if server.ConnectTimeout != 0 {
- smtpConnectChannel = make(chan error, 2)
- go func() {
- c, err = smtpConnect(server.Host, fmt.Sprintf("%d", server.Port), a, server.Encryption, new(tls.Config))
- // send the result
- smtpConnectChannel <- err
- }()
- }
- if server.ConnectTimeout == 0 {
- // no ConnectTimeout, just fire the connect
- c, err = smtpConnect(server.Host, fmt.Sprintf("%d", server.Port), a, server.Encryption, new(tls.Config))
- } else {
- // get the connect result or timeout result, which ever happens first
- select {
- case err = <-smtpConnectChannel:
- if err != nil {
- return nil, errors.New(err.Error())
- }
- case <-time.After(server.ConnectTimeout):
- return nil, errors.New("Mail Error: SMTP Connection timed out")
- }
- }
- return &SMTPClient{
- Client: c,
- KeepAlive: server.KeepAlive,
- SendTimeout: server.SendTimeout,
- }, nil
- }
- // Reset send RSET command to smtp client
- func (smtpClient *SMTPClient) Reset() error {
- return smtpClient.Client.reset()
- }
- // Noop send NOOP command to smtp client
- func (smtpClient *SMTPClient) Noop() error {
- return smtpClient.Client.noop()
- }
- // Quit send QUIT command to smtp client
- func (smtpClient *SMTPClient) Quit() error {
- return smtpClient.Client.quit()
- }
- // Close closes the connection
- func (smtpClient *SMTPClient) Close() error {
- return smtpClient.Client.close()
- }
- // send does the low level sending of the email
- func send(from string, to []string, msg string, client *SMTPClient) error {
- //Check if client struct is not nil
- if client != nil {
- //Check if client is not nil
- if client.Client != nil {
- var smtpSendChannel chan error
- // if there is a SendTimeout, setup the channel and do the send under a goroutine
- if client.SendTimeout != 0 {
- smtpSendChannel = make(chan error, 1)
- go func(from string, to []string, msg string, c *smtpClient) {
- smtpSendChannel <- sendMailProcess(from, to, msg, c)
- }(from, to, msg, client.Client)
- }
- if client.SendTimeout == 0 {
- // no SendTimeout, just fire the sendMailProcess
- return sendMailProcess(from, to, msg, client.Client)
- }
- // get the send result or timeout result, which ever happens first
- select {
- case sendError := <-smtpSendChannel:
- checkKeepAlive(client)
- return sendError
- case <-time.After(client.SendTimeout):
- checkKeepAlive(client)
- return errors.New("Mail Error: SMTP Send timed out")
- }
- }
- }
- return errors.New("Mail Error: No SMTP Client Provided")
- }
- func sendMailProcess(from string, to []string, msg string, c *smtpClient) error {
- // Set the sender
- if err := c.mail(from); err != nil {
- return err
- }
- // Set the recipients
- for _, address := range to {
- if err := c.rcpt(address); err != nil {
- return err
- }
- }
- // Send the data command
- w, err := c.data()
- if err != nil {
- return err
- }
- // write the message
- _, err = fmt.Fprint(w, msg)
- if err != nil {
- return err
- }
- err = w.Close()
- if err != nil {
- return err
- }
- return nil
- }
- //check if keepAlive for close or reset
- func checkKeepAlive(client *SMTPClient) {
- if client.KeepAlive {
- client.Client.reset()
- } else {
- client.Client.quit()
- client.Client.close()
- }
- }
|