main.go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
package main

import (
	"bytes"
	"crypto/tls"
	"encoding/base64"
	"fmt"
	"io/ioutil"
	"mime/multipart"
	"net/smtp"
	"path/filepath"
	"strings"
)

// EmailConfig contains SMTP server configuration and email details
type EmailConfig struct {
	SMTPHost     string
	SMTPPort     int
	Username     string
	Password     string
	From         string
	To           []string
	CC           []string
	BCC          []string
	Subject      string
	Body         string
	IsHTML       bool
	Attachments  []string
}

// SendEmail sends an email with optional attachments and HTML content
func SendEmail(config EmailConfig) error {
	// Create email headers
	headers := make(map[string]string)
	headers["From"] = config.From
	headers["To"] = strings.Join(config.To, ",")
	if len(config.CC) > 0 {
		headers["Cc"] = strings.Join(config.CC, ",")
	}
	headers["Subject"] = config.Subject
	headers["MIME-Version"] = "1.0"

	// Create multipart writer
	var body bytes.Buffer
	writer := multipart.NewWriter(&body)
	boundary := writer.Boundary()
	headers["Content-Type"] = fmt.Sprintf("multipart/mixed; boundary=\"%s\"", boundary)

	// Write email body
	contentType := "text/plain; charset=utf-8"
	if config.IsHTML {
		contentType = "text/html; charset=utf-8"
	}

	part, err := writer.CreatePart(map[string]string{
		"Content-Type": contentType,
	})
	if err != nil {
		return fmt.Errorf("failed to create body part: %w", err)
	}
	_, err = part.Write([]byte(config.Body))
	if err != nil {
		return fmt.Errorf("failed to write body: %w", err)
	}

	// Add attachments
	for _, file := range config.Attachments {
		if err := addAttachment(writer, file); err != nil {
			return fmt.Errorf("failed to add attachment %s: %w", file, err)
		}
	}

	// Close writer to finalize multipart message
	if err := writer.Close(); err != nil {
		return fmt.Errorf("failed to close writer: %w", err)
	}

	// Combine all recipients
	recipients := append(config.To, config.CC...)
	recipients = append(recipients, config.BCC...)

	// Prepare auth
	auth := smtp.PlainAuth("", config.Username, config.Password, config.SMTPHost)

	// Connect to SMTP server with TLS
	addr := fmt.Sprintf("%s:%d", config.SMTPHost, config.SMTPPort)
	tlsConfig := &tls.Config{
		ServerName: config.SMTPHost,
	}

	conn, err := tls.Dial("tcp", addr, tlsConfig)
	if err != nil {
		return fmt.Errorf("tls dial failed: %w", err)
	}

	client, err := smtp.NewClient(conn, config.SMTPHost)
	if err != nil {
		return fmt.Errorf("smtp client creation failed: %w", err)
	}
	defer client.Close()

	// Authenticate
	if err := client.Auth(auth); err != nil {
		return fmt.Errorf("authentication failed: %w", err)
	}

	// Set sender and recipients
	if err := client.Mail(config.From); err != nil {
		return fmt.Errorf("failed to set sender: %w", err)
	}

	for _, rec := range recipients {
		if err := client.Rcpt(rec); err != nil {
			return fmt.Errorf("failed to set recipient %s: %w", rec, err)
		}
	}

	// Send email body
	writer, err = client.Data()
	if err != nil {
		return fmt.Errorf("failed to get data writer: %w", err)
	}

	// Write headers
	for k, v := range headers {
		_, err := writer.Write([]byte(fmt.Sprintf("%s: %s\r\n", k, v)))
		if err != nil {
			return fmt.Errorf("failed to write header %s: %w", k, err)
		}
	}
	// Write empty line to separate headers from body
	_, err = writer.Write([]byte("\r\n"))
	if err != nil {
		return fmt.Errorf("failed to write header-body separator: %w", err)
	}

	// Write body
	_, err = writer.Write(body.Bytes())
	if err != nil {
		return fmt.Errorf("failed to write email body: %w", err)
	}

	// Close writer to send email
	if err := writer.Close(); err != nil {
		return fmt.Errorf("failed to close data writer: %w", err)
	}

	return client.Quit()
}

// addAttachment adds a file attachment to the email
func addAttachment(writer *multipart.Writer, filePath string) error {
	fileData, err := ioutil.ReadFile(filePath)
	if err != nil {
		return fmt.Errorf("failed to read file: %w", err)
	}

	fileName := filepath.Base(filePath)
	part, err := writer.CreatePart(map[string]string{
		"Content-Type":        "application/octet-stream",
		"Content-Disposition": fmt.Sprintf("attachment; filename=\"%s\"", fileName),
		"Content-Transfer-Encoding": "base64",
	})
	if err != nil {
		return fmt.Errorf("failed to create attachment part: %w", err)
	}

	encoder := base64.NewEncoder(base64.StdEncoding, part)
	defer encoder.Close()

	_, err = encoder.Write(fileData)
	if err != nil {
		return fmt.Errorf("failed to encode attachment: %w", err)
	}

	return nil
}

func main() {
	config := EmailConfig{
		SMTPHost:    "smtp.gmail.com",
		SMTPPort:    465,
		Username:    "[email protected]",
		Password:    "your-app-password",
		From:        "[email protected]",
		To:          []string{"[email protected]"},
		CC:          []string{"[email protected]"},
		Subject:     "Test Email with Attachment",
		Body:        "<h1>Hello!</h1><p>This is an HTML email with attachment.</p>",
		IsHTML:      true,
		Attachments: []string{"document.pdf"},
	}

	if err := SendEmail(config); err != nil {
		fmt.Printf("Failed to send email: %v\n", err)
	} else {
		fmt.Println("Email sent successfully!")
	}
}

How It Works

SMTP client helper that sends text or HTML emails with attachments, CC or BCC, and TLS configuration.

Builds MIME messages with boundaries, attaches files by reading disk content, sets headers for recipients including CC and BCC, connects over TLS using net/smtp, and sends the composed message.

Key Concepts

  • 1Supports multipart or alternative bodies for HTML plus plain text.
  • 2Attachment encoding handles arbitrary file types safely.
  • 3TLS dialer ensures credentials and content are encrypted in transit.

When to Use This Pattern

  • Transactional email sending from backend services.
  • Automated reports with PDF or CSV attachments.
  • Testing SMTP integrations locally with mock servers.

Best Practices

  • Use application-specific passwords or secrets from vaults.
  • Set proper From and Reply-To headers to reduce spam scoring.
  • Retry transient SMTP errors with backoff.
Go Version1.16
Difficultyintermediate
Production ReadyYes
Lines of Code199