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))
}
@ryanprtma
Copy link

i tried this with mailtrap as :

Screenshot from 2022-03-09 07-28-15

but i got this

Screenshot from 2022-03-09 07-28-57

@stvoidit
Copy link

stvoidit commented Mar 9, 2022

i tried this with mailtrap as :

Screenshot from 2022-03-09 07-28-15

but i got this

Screenshot from 2022-03-09 07-28-57

https://github.com/stvoidit/gosmtp

Please try my version based on the example of this code. Due to the specific situation, I have to use my own implementation, because popular solutions for some reason do not work, although I have implemented very basic things and perhaps not in the best way, but it works for me.

@bekha-io
Copy link

bekha-io commented Sep 30, 2022

Hi @AddyM I made a small change that should help you with that, I tested it and it worked, also you can contact me directly, I will gladly give you a hand.

https://twitter.com/douglasmakey [email protected]

Hi, @douglasmakey ! I'm getting the same error as the author above. This code results only empty .csv files... :(

@rahuljoshi897
Copy link

H @douglasmakey , Is there any way to include an image in the email body using the GOalng net/smtp package?

@suraneti
Copy link

io/ioutils has deprecated after go 1.16

package main

import (
	"bytes"
	"encoding/base64"
	"fmt"
	"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 := os.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