Skip to content

Instantly share code, notes, and snippets.

@douglasmakey
Last active January 26, 2025 01:24
Show Gist options
  • Save douglasmakey/90753ecf37ac10c25873825097f46300 to your computer and use it in GitHub Desktop.
Save douglasmakey/90753ecf37ac10c25873825097f46300 to your computer and use it in GitHub Desktop.
Golang - send an email with attachments.
package main
import (
"bytes"
"encoding/base64"
"fmt"
"io/ioutil"
"mime/multipart"
"net/smtp"
"os"
"path/filepath"
)
var (
host = os.Getenv("EMAIL_HOST")
username = os.Getenv("EMAiL_USERNAME")
password = os.Getenv("EMAIL_PASSWORD")
portNumber = os.Getenv("EMAIL_PORT")
)
type Sender struct {
auth smtp.Auth
}
type Message struct {
To []string
CC []string
BCC []string
Subject string
Body string
Attachments map[string][]byte
}
func New() *Sender {
auth := smtp.PlainAuth("", username, password, host)
return &Sender{auth}
}
func (s *Sender) Send(m *Message) error {
return smtp.SendMail(fmt.Sprintf("%s:%s", host, portNumber), s.auth, username, m.To, m.ToBytes())
}
func NewMessage(s, b string) *Message {
return &Message{Subject: s, Body: b, Attachments: make(map[string][]byte)}
}
func (m *Message) AttachFile(src string) error {
b, err := ioutil.ReadFile(src)
if err != nil {
return err
}
_, fileName := filepath.Split(src)
m.Attachments[fileName] = b
return nil
}
func (m *Message) ToBytes() []byte {
buf := bytes.NewBuffer(nil)
withAttachments := len(m.Attachments) > 0
buf.WriteString(fmt.Sprintf("Subject: %s\n", m.Subject))
buf.WriteString(fmt.Sprintf("To: %s\n", strings.Join(m.To, ",")))
if len(m.CC) > 0 {
buf.WriteString(fmt.Sprintf("Cc: %s\n", strings.Join(m.CC, ",")))
}
if len(m.BCC) > 0 {
buf.WriteString(fmt.Sprintf("Bcc: %s\n", strings.Join(m.BCC, ",")))
}
buf.WriteString("MIME-Version: 1.0\n")
writer := multipart.NewWriter(buf)
boundary := writer.Boundary()
if withAttachments {
buf.WriteString(fmt.Sprintf("Content-Type: multipart/mixed; boundary=%s\n", boundary))
buf.WriteString(fmt.Sprintf("--%s\n", boundary))
} else {
buf.WriteString("Content-Type: text/plain; charset=utf-8\n")
}
buf.WriteString(m.Body)
if withAttachments {
for k, v := range m.Attachments {
buf.WriteString(fmt.Sprintf("\n\n--%s\n", boundary))
buf.WriteString(fmt.Sprintf("Content-Type: %s\n", http.DetectContentType(v)))
buf.WriteString("Content-Transfer-Encoding: base64\n")
buf.WriteString(fmt.Sprintf("Content-Disposition: attachment; filename=%s\n", k))
b := make([]byte, base64.StdEncoding.EncodedLen(len(v)))
base64.StdEncoding.Encode(b, v)
buf.Write(b)
buf.WriteString(fmt.Sprintf("\n--%s", boundary))
}
buf.WriteString("--")
}
return buf.Bytes()
}
func main() {
sender := New()
m := NewMessage("Test", "Body message.")
m.To = []string{"[email protected]"}
m.CC = []string{"[email protected]", "[email protected]"}
m.BCC = []string{"[email protected]"}
m.AttachFile("/path/to/file")
fmt.Println(sender.Send(m))
}
@imrinzzzz
Copy link

Just wanna say thank you so much for this

@zyriab
Copy link

zyriab commented Jan 26, 2025

If anybody is passing by, you can use

mime.TypeByExtension(filepath.Ext(filename))

instead of http.DetectContentType(byteSlice).

You'll likely have better results as the http function supports less MIME types.
Be aware, though, that the first function (from package mime) uses the file extension, which means that it can be tricked easily.
On the other hand, it seems that some file types have the same beginning bytes which will confuse the http function (.docx, .xls, etc apparently are mistaken for .zip, iirc).

Double check the last info but that should give you a good starting point to fix some of the issues people seemed to encounter in the comments above.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment