207 lines
4.9 KiB
Go
207 lines
4.9 KiB
Go
package smtp
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"mime/multipart"
|
|
"net/smtp"
|
|
"net/textproto"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type Auth struct {
|
|
Host string
|
|
Port string
|
|
Pass string
|
|
Debug bool
|
|
}
|
|
|
|
func Client(host, port, password string) *Auth {
|
|
return &Auth{
|
|
Host: host,
|
|
Port: port,
|
|
Pass: password,
|
|
}
|
|
}
|
|
|
|
func (s *Auth) tlsConfig() *tls.Config {
|
|
return &tls.Config{ServerName: s.Host}
|
|
}
|
|
|
|
func (s *Auth) smtpClient() (*smtp.Client, error) {
|
|
address := s.Host + ":" + s.Port
|
|
portNum, err := strconv.Atoi(s.Port)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("invalid port number: %w", err)
|
|
}
|
|
|
|
if portNum == 465 {
|
|
conn, err := tls.Dial("tcp", address, s.tlsConfig())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to connect to SMTPS server: %w", err)
|
|
}
|
|
return smtp.NewClient(conn, s.Host)
|
|
}
|
|
|
|
client, err := smtp.Dial(address)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to connect to SMTP server: %w", err)
|
|
}
|
|
|
|
if ok, _ := client.Extension("STARTTLS"); ok {
|
|
if err := client.StartTLS(s.tlsConfig()); err != nil {
|
|
client.Close()
|
|
return nil, fmt.Errorf("failed to start TLS: %w", err)
|
|
}
|
|
} else {
|
|
client.Close()
|
|
return nil, fmt.Errorf("TLS not supported by the server")
|
|
}
|
|
|
|
return client, nil
|
|
}
|
|
|
|
func (s *Auth) SendText(
|
|
from string,
|
|
to []string,
|
|
subject string,
|
|
body string,
|
|
attachments map[string]*bytes.Buffer,
|
|
) error {
|
|
return s.sendMessage(from, to, subject, body, "text/plain; charset=utf-8", attachments)
|
|
}
|
|
|
|
func (s *Auth) SendHTML(
|
|
from string,
|
|
to []string,
|
|
subject string,
|
|
html string,
|
|
attachments map[string]*bytes.Buffer,
|
|
) error {
|
|
return s.sendMessage(from, to, subject, html, "text/html; charset=utf-8", attachments)
|
|
}
|
|
|
|
func (s *Auth) sendMessage(
|
|
from string,
|
|
to []string,
|
|
subject string,
|
|
body string,
|
|
contentType string,
|
|
attachments map[string]*bytes.Buffer,
|
|
) error {
|
|
message, err := s.buildMessage(from, to, subject, body, contentType, attachments)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create email message: %w", err)
|
|
}
|
|
|
|
client, err := s.smtpClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer client.Close()
|
|
|
|
auth := smtp.PlainAuth("", from, s.Pass, s.Host)
|
|
if err = client.Auth(auth); err != nil {
|
|
return fmt.Errorf("authentication failed: %w", err)
|
|
}
|
|
|
|
if err = client.Mail(from); err != nil {
|
|
return fmt.Errorf("failed to set sender: %w", err)
|
|
}
|
|
for _, recipient := range to {
|
|
if err = client.Rcpt(recipient); err != nil {
|
|
return fmt.Errorf("failed to add recipient %s: %w", recipient, err)
|
|
}
|
|
}
|
|
|
|
w, err := client.Data()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get data writer: %w", err)
|
|
}
|
|
if _, err = w.Write(message); err != nil {
|
|
return fmt.Errorf("failed to write message: %w", err)
|
|
}
|
|
if err = w.Close(); err != nil {
|
|
return fmt.Errorf("failed to send message: %w", err)
|
|
}
|
|
|
|
return client.Quit()
|
|
}
|
|
|
|
func (s *Auth) Validate(user string) error {
|
|
client, err := s.smtpClient()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer client.Close()
|
|
|
|
auth := smtp.PlainAuth("", user, s.Pass, s.Host)
|
|
if err := client.Auth(auth); err != nil {
|
|
return fmt.Errorf("authentication failed: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *Auth) buildMessage(
|
|
from string,
|
|
to []string,
|
|
subject string,
|
|
body string,
|
|
contentType string,
|
|
attachments map[string]*bytes.Buffer,
|
|
) ([]byte, error) {
|
|
var buf bytes.Buffer
|
|
writer := multipart.NewWriter(&buf)
|
|
|
|
buf.WriteString(fmt.Sprintf("From: %s\r\n", from))
|
|
buf.WriteString(fmt.Sprintf("To: %s\r\n", strings.Join(to, ",")))
|
|
buf.WriteString(fmt.Sprintf("Subject: %s\r\n", subject))
|
|
buf.WriteString("MIME-Version: 1.0\r\n")
|
|
buf.WriteString(fmt.Sprintf("Content-Type: multipart/mixed; boundary=%s\r\n\r\n", writer.Boundary()))
|
|
|
|
if err := s.write(writer, contentType, body); err != nil {
|
|
return nil, fmt.Errorf("failed to write email body: %w", err)
|
|
}
|
|
|
|
for filename, fileBuffer := range attachments {
|
|
if err := s.attach(writer, filename, fileBuffer); err != nil {
|
|
return nil, fmt.Errorf("failed to attach file %s: %w", filename, err)
|
|
}
|
|
}
|
|
|
|
if err := writer.Close(); err != nil {
|
|
return nil, fmt.Errorf("failed to close writer: %w", err)
|
|
}
|
|
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func (s *Auth) write(writer *multipart.Writer, contentType, content string) error {
|
|
partHeader := make(textproto.MIMEHeader)
|
|
partHeader.Set("Content-Type", contentType)
|
|
part, err := writer.CreatePart(partHeader)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create part: %w", err)
|
|
}
|
|
|
|
_, err = part.Write([]byte(content))
|
|
return err
|
|
}
|
|
|
|
func (s *Auth) attach(writer *multipart.Writer, filename string, fileBuffer *bytes.Buffer) error {
|
|
attachmentHeader := make(textproto.MIMEHeader)
|
|
attachmentHeader.Set("Content-Type", "application/octet-stream")
|
|
attachmentHeader.Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filename))
|
|
attachment, err := writer.CreatePart(attachmentHeader)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to create attachment part: %w", err)
|
|
}
|
|
|
|
_, err = attachment.Write(fileBuffer.Bytes())
|
|
return err
|
|
}
|