github.com/blend/go-sdk@v1.20220411.3/email/smtp_sender.go (about)

     1  /*
     2  
     3  Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved
     4  Use of this source code is governed by a MIT license that can be found in the LICENSE file.
     5  
     6  */
     7  
     8  package email
     9  
    10  import (
    11  	"bufio"
    12  	"context"
    13  	"crypto/tls"
    14  	"fmt"
    15  	"net/smtp"
    16  
    17  	"github.com/blend/go-sdk/configutil"
    18  	"github.com/blend/go-sdk/env"
    19  	"github.com/blend/go-sdk/ex"
    20  )
    21  
    22  var (
    23  	_ Sender = (*SMTPSender)(nil)
    24  )
    25  
    26  // SMTPSender is a sender for emails over smtp.
    27  type SMTPSender struct {
    28  	LocalName string        `json:"localname" yaml:"localname"`
    29  	Host      string        `json:"host" yaml:"host" env:"SMTP_HOST"`
    30  	Port      string        `json:"port" yaml:"port" env:"SMTP_PORT"`
    31  	PlainAuth SMTPPlainAuth `json:"plainAuth" yaml:"plainAuth"`
    32  }
    33  
    34  // Resolve implements configutil.ConfigResolver.
    35  func (s *SMTPSender) Resolve(ctx context.Context) error {
    36  	return configutil.Resolve(ctx,
    37  		func(ictx context.Context) error { return env.GetVars(ictx).ReadInto(s) },
    38  		s.PlainAuth.Resolve,
    39  	)
    40  }
    41  
    42  // IsZero returns if the smtp sender is set or not.
    43  func (s SMTPSender) IsZero() bool {
    44  	return s.Host == ""
    45  }
    46  
    47  // PortOrDefault returns a property or a default.
    48  func (s SMTPSender) PortOrDefault() string {
    49  	if s.Port != "" {
    50  		return s.Port
    51  	}
    52  	return "465"
    53  }
    54  
    55  // LocalNameOrDefault returns a property or a default.
    56  func (s SMTPSender) LocalNameOrDefault() string {
    57  	if s.LocalName != "" {
    58  		return s.LocalName
    59  	}
    60  	return s.Host
    61  }
    62  
    63  // Send sends an email via. smtp.
    64  func (s SMTPSender) Send(ctx context.Context, message Message) error {
    65  	if s.Host == "" {
    66  		return ex.New("smtp host unset")
    67  	}
    68  	if err := message.Validate(); err != nil {
    69  		return err
    70  	}
    71  
    72  	tlsConfig := &tls.Config{ServerName: s.Host, InsecureSkipVerify: true}
    73  	conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%s", s.Host, s.PortOrDefault()), tlsConfig)
    74  	if err != nil {
    75  		return ex.New(err)
    76  	}
    77  
    78  	client, err := smtp.NewClient(conn, s.Host)
    79  	if err != nil {
    80  		return ex.New(err)
    81  	}
    82  	defer client.Close()
    83  
    84  	if err := client.Hello(s.LocalNameOrDefault()); err != nil {
    85  		return ex.New(err)
    86  	}
    87  	if !s.PlainAuth.IsZero() {
    88  		if err := client.Auth(smtp.PlainAuth(s.PlainAuth.Identity, s.PlainAuth.Username, s.PlainAuth.Password, s.Host)); err != nil {
    89  			return ex.New(err)
    90  		}
    91  	}
    92  	if err := client.Mail(message.From); err != nil {
    93  		return ex.New(err)
    94  	}
    95  
    96  	for _, to := range message.To {
    97  		if err := client.Rcpt(to); err != nil {
    98  			return ex.New(err)
    99  		}
   100  	}
   101  	for _, cc := range message.CC {
   102  		if err := client.Rcpt(cc); err != nil {
   103  			return ex.New(err)
   104  		}
   105  	}
   106  	for _, bcc := range message.BCC {
   107  		if err := client.Rcpt(bcc); err != nil {
   108  			return ex.New(err)
   109  		}
   110  	}
   111  
   112  	w, err := client.Data()
   113  	if err != nil {
   114  		return ex.New(err)
   115  	}
   116  
   117  	// msg data
   118  	bufWriter := bufio.NewWriter(w)
   119  	if _, err := bufWriter.WriteString(fmt.Sprintf("From: %s\r\n", message.From)); err != nil {
   120  		return ex.New(err)
   121  	}
   122  	if err = bufWriter.Flush(); err != nil {
   123  		return ex.New(err)
   124  	}
   125  	for _, to := range message.To {
   126  		if _, err := bufWriter.WriteString(fmt.Sprintf("To: %s\r\n", to)); err != nil {
   127  			return ex.New(err)
   128  		}
   129  	}
   130  	if err = bufWriter.Flush(); err != nil {
   131  		return ex.New(err)
   132  	}
   133  	for _, cc := range message.CC {
   134  		if _, err := bufWriter.WriteString(fmt.Sprintf("Cc: %s\r\n", cc)); err != nil {
   135  			return ex.New(err)
   136  		}
   137  	}
   138  	if err = bufWriter.Flush(); err != nil {
   139  		return ex.New(err)
   140  	}
   141  	if message.Subject != "" {
   142  		if _, err := bufWriter.WriteString("Subject: " + message.Subject + "\r\n"); err != nil {
   143  			return ex.New(err)
   144  		}
   145  	}
   146  	if err = bufWriter.Flush(); err != nil {
   147  		return ex.New(err)
   148  	}
   149  
   150  	if message.HTMLBody != "" {
   151  		if _, err := bufWriter.WriteString("MIME-version: 1.0;\r\nContent-Type: text/html; charset=\"UTF-8\";\r\n"); err != nil {
   152  			return ex.New(err)
   153  		}
   154  		if _, err := bufWriter.WriteString(message.HTMLBody); err != nil {
   155  			return ex.New(err)
   156  		}
   157  	} else if message.TextBody != "" {
   158  		if _, err := bufWriter.WriteString(message.TextBody); err != nil {
   159  			return ex.New(err)
   160  		}
   161  	}
   162  	if err = bufWriter.Flush(); err != nil {
   163  		return ex.New(err)
   164  	}
   165  	if err := w.Close(); err != nil {
   166  		return ex.New(err)
   167  	}
   168  
   169  	return ex.New(client.Quit())
   170  }
   171  
   172  // SMTPPlainAuth is a auth set for smtp.
   173  type SMTPPlainAuth struct {
   174  	Identity string `json:"identity" yaml:"identity"`
   175  	Username string `json:"username" yaml:"username" env:"SMTP_USERNAME"`
   176  	Password string `json:"password" yaml:"password" env:"SMTP_PASSWORD"`
   177  }
   178  
   179  // Resolve implements configutil.ConfigResolver.
   180  func (spa SMTPPlainAuth) Resolve(ctx context.Context) error {
   181  	return env.GetVars(ctx).ReadInto(&spa) //note(wc); i'm not sure this will always work
   182  }
   183  
   184  // IsZero returns if the plain auth is unset.
   185  func (spa SMTPPlainAuth) IsZero() bool {
   186  	return spa.Username == "" && spa.Password == ""
   187  }