Skip to content

Instantly share code, notes, and snippets.

@arouene
Last active May 22, 2021 23:31
Show Gist options
  • Save arouene/93f685a21b3af0e3d091bb4d8636b1ec to your computer and use it in GitHub Desktop.
Save arouene/93f685a21b3af0e3d091bb4d8636b1ec to your computer and use it in GitHub Desktop.
Send emails using SMTP with TLS support in Go using standard libraries only
// Connecting to a SMTP server to send an email in Go
// - Support TLS and optional password
// - A CA file can be provided or it will use the CA bundle of your OS
//
// Usage:
// s := smtp.NewSMTP("localhost", "[email protected]", "[email protected]")
// s.SetPort(465)
// s.SetTLS(true)
// s.SetUser("login/user")
// s.SetPassword("password")
// s.SetCAFile("/path/to/cafile")
// s.Send("Subject", "Body)
//
// Additionnal recipents can be added with
//
// s.AddRecv("[email protected]")
//
package smtp
import (
"bytes"
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"log"
"net"
"net/mail"
"net/smtp"
"os"
"strconv"
"strings"
"time"
)
type SMTP struct {
from *mail.Address
to []*mail.Address
smtpHost string
smtpPort string
smtpTLS bool
smtpUser string
smtpPassword string
caFile string
}
//func NewSMTP(from string, to []string, smtpHost string) SMTP {
func NewSMTP(host, from string, rcpt []string) *SMTP {
s := SMTP{
smtpPort: "25",
smtpTLS: true,
}
s.SetHost(host)
s.SetFrom(from)
for _, m := range rcpt {
s.AddRecv(m)
}
return &s
}
func (s *SMTP) SetFrom(address string) *SMTP {
addr, err := mail.ParseAddress(address)
if err != nil {
log.Fatalf("%s is not a valid email address", address)
}
s.from = addr
return s
}
// AddRecv allow to specify multiple recipents
//
// s.AddRecv("[email protected]")
// s.AddRecv("[email protected]")
//
func (s *SMTP) AddRecv(address string) *SMTP {
addr, err := mail.ParseAddress(address)
if err != nil {
log.Fatalf("%s is not a valid email address", address)
}
s.to = append(s.to, addr)
return s
}
// SetHost set the smtp host "smtp.example.org"
func (s *SMTP) SetHost(h string) *SMTP {
s.smtpHost = h
return s
}
// SetPort set the smtp port : 25, 465, 857, ...
// 25 is the default port if not specified
func (s *SMTP) SetPort(port int64) *SMTP {
if port < 1 || port > 65535 {
log.Fatalf("Port must be between 1 and 65535, got %d\n", port)
}
s.smtpPort = strconv.FormatInt(port, 10)
return s
}
// SetTLS set the connection mode, true for TLS or false without.
// smtpTLS is true by default, set false to disable
func (s *SMTP) SetTLS(b bool) *SMTP {
s.smtpTLS = b
return s
}
func (s *SMTP) SetUser(user string) *SMTP {
s.smtpUser = user
return s
}
func (s *SMTP) SetPassword(password string) *SMTP {
s.smtpPassword = password
return s
}
func (s *SMTP) SetCAFile(file string) *SMTP {
if _, err := os.Stat(file); os.IsNotExist(err) {
log.Fatalf("CAFile %s does not exists\n", file)
}
s.caFile = file
return s
}
func (s *SMTP) Send(subj, body string) error {
// Panic if mandatory parameters are not set
if s.from.Address == "" || len(s.to) <= 0 || s.smtpHost == "" {
log.Panic("SMTP needs a smtpHost, a from and a recv parameter")
}
// Setup headers
headers := make(map[string]string)
headers["From"] = s.from.String()
headers["To"] = AddressesToString(s.to)
headers["Subject"] = subj
// Setup message
var message bytes.Buffer
for k, v := range headers {
message.WriteString(fmt.Sprintf("%s: %s\r\n", k, v))
}
message.WriteString("\r\n" + body)
var err error
var conn net.Conn
var smtpConn *smtp.Client
var addr = s.smtpHost + ":" + s.smtpPort
var timeout = time.Second * 10
// Establish a connection with the smtp server
if conn, err = net.DialTimeout("tcp", addr, timeout); err != nil {
return err
}
// Support of TLS if enabled
if s.smtpTLS {
var rootCAs *x509.CertPool
// Custom CA file if set
if s.caFile != "" {
rootCAs = x509.NewCertPool()
pemBytes, err := ioutil.ReadFile(s.caFile)
if err != nil {
return err
}
ok := rootCAs.AppendCertsFromPEM(pemBytes)
if !ok {
return err
}
}
// TLS config
tlsconfig := &tls.Config{
InsecureSkipVerify: false,
ServerName: s.smtpHost,
RootCAs: rootCAs,
}
tlsConn := tls.Client(conn, tlsconfig)
if err = tlsConn.Handshake(); err != nil {
return err
}
if smtpConn, err = smtp.NewClient(tlsConn, s.smtpHost); err != nil {
return err
}
} else {
if smtpConn, err = smtp.NewClient(conn, s.smtpHost); err != nil {
return err
}
}
defer smtpConn.Close()
// Auth
if s.smtpUser != "" && s.smtpPassword != "" {
auth := smtp.PlainAuth("", s.smtpUser, s.smtpPassword, s.smtpHost)
if err = smtpConn.Auth(auth); err != nil {
return err
}
}
// From
if err = smtpConn.Mail(s.from.Address); err != nil {
return err
}
// To
for _, addr := range s.to {
if err = smtpConn.Rcpt(addr.Address); err != nil {
log.Printf("Error while setting the RCPT: %s\n", err.Error())
}
}
// Data
w, err := smtpConn.Data()
if err != nil {
return err
}
// Effectively send the message
_, err = w.Write(message.Bytes())
if err != nil {
return err
} else {
log.Println("Mail sent successfully")
}
err = w.Close()
if err != nil {
log.Println(err.Error())
}
smtpConn.Quit()
return nil
}
func AddressesToString(addresses []*mail.Address) string {
var l []string
for _, addr := range addresses {
l = append(l, addr.String())
}
return strings.Join(l, ", ")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment