Viesti Sign in

Send email from Go with net/smtp

Go's standard net/smtp package is enough to send transactional email through Viesti: smtp.SendMail upgrades to TLS with STARTTLS on port 587, and smtp.PlainAuth refuses to send the key over an unencrypted connection.

Before you start

  1. Sign in and verify the domain you send from. Viesti accepts a message only when its From address is on a domain verified in your workspace and the envelope sender (MAIL FROM), which most libraries take from the From address, is on the same domain. DNS setup lists the records.
  2. Create an API key on the Credentials page of the dashboard. It is shown once. The key is the SMTP password; the username is apikey.
Setting Value
Host smtp.viesti.email
Port 587 with STARTTLS, or 465 with TLS from the first byte
Username apikey
Password Your API key: mr_ followed by 64 hex characters
Authentication PLAIN, offered only once the connection is encrypted

Set the environment

export VIESTI_API_KEY="mr_your_api_key"
export MAIL_FROM="[email protected]"

SendMail passes the sender to the SMTP MAIL FROM command as is, so use a bare address without a display name.

Send an email

package main

import (
	"log"
	"net/smtp"
	"os"
)

func main() {
	const host = "smtp.viesti.email"
	from := os.Getenv("MAIL_FROM") // bare address on your verified domain
	to := []string{"[email protected]"}

	msg := []byte("From: " + from + "\r\n" +
		"To: [email protected]\r\n" +
		"Subject: Your receipt\r\n" +
		"MIME-Version: 1.0\r\n" +
		"Content-Type: text/plain; charset=utf-8\r\n" +
		"\r\n" +
		"Thanks for your order.\r\n")

	auth := smtp.PlainAuth("", "apikey", os.Getenv("VIESTI_API_KEY"), host)
	if err := smtp.SendMail(host+":587", auth, from, to, msg); err != nil {
		log.Fatal(err)
	}
	log.Print("accepted")
}

Viesti adds Date and Message-ID headers when a message has none. Tested with Go 1.26.

Test it

go run .

It logs accepted when Viesti takes the message. Check Activity in the dashboard.

Port 465

SendMail only speaks STARTTLS. On 465 TLS starts before the SMTP greeting, so dial with crypto/tls and drive the client yourself:

package main

import (
	"crypto/tls"
	"log"
	"net/smtp"
	"os"
)

func main() {
	const host = "smtp.viesti.email"
	from := os.Getenv("MAIL_FROM") // bare address on your verified domain
	to := []string{"[email protected]"}

	msg := []byte("From: " + from + "\r\n" +
		"To: [email protected]\r\n" +
		"Subject: Your receipt\r\n" +
		"MIME-Version: 1.0\r\n" +
		"Content-Type: text/plain; charset=utf-8\r\n" +
		"\r\n" +
		"Thanks for your order.\r\n")

	if err := sendImplicitTLS(host, from, to, msg); err != nil {
		log.Fatal(err)
	}
	log.Print("accepted")
}

// sendImplicitTLS submits msg on port 465, where TLS starts before the
// SMTP greeting; smtp.SendMail only speaks STARTTLS.
func sendImplicitTLS(host, from string, to []string, msg []byte) error {
	conn, err := tls.Dial("tcp", host+":465", &tls.Config{ServerName: host})
	if err != nil {
		return err
	}
	c, err := smtp.NewClient(conn, host)
	if err != nil {
		return err
	}
	defer c.Close()
	if err = c.Auth(smtp.PlainAuth("", "apikey", os.Getenv("VIESTI_API_KEY"), host)); err != nil {
		return err
	}
	if err = c.Mail(from); err != nil {
		return err
	}
	for _, addr := range to {
		if err = c.Rcpt(addr); err != nil {
			return err
		}
	}
	w, err := c.Data()
	if err != nil {
		return err
	}
	if _, err = w.Write(msg); err != nil {
		return err
	}
	if err = w.Close(); err != nil {
		return err
	}
	return c.Quit()
}

Why net/smtp

The package is frozen: its documentation says it accepts no new features. It still covers submission: STARTTLS when the server offers it, PLAIN authentication that is only sent over TLS or to localhost, and the SMTP commands. What it lacks is a message builder and timeouts. For HTML with a text alternative or attachments, build the body with mime/multipart or a third-party package; the connection code stays the same. For a deadline, open the connection yourself with a net.Dialer that sets Timeout (for port 465, tls.DialWithDialer) and hand it to smtp.NewClient, as the port 465 example hands over its connection.

Common errors

Error Cause and fix
501 5.5.2 Was expecting MAIL arg syntax of FROM:<address> MAIL_FROM contains a display name such as Acme <receipts@…>. Pass a bare address to SendMail; put the display name in the From: header only.
wrong host name The host given to PlainAuth differs from the host in the address given to SendMail, for example because it includes the port. Use the same host string for both.
No output; the program hangs SendMail to port 465. It waits for a plain-text greeting that never comes and has no timeout. Use port 587, or the implicit TLS client above.

Replies from Viesti

Whatever the library, its error includes the server's reply. These are the ones caused by your setup or account:

Reply Meaning
535 5.7.8 Authentication failed The API key is wrong, revoked or empty. After ten failed attempts from one client address within ten minutes, that address is refused for ten minutes with 454 Too many failed authentications; retry later.
525 5.7.13 Workspace suspended; contact support The key is valid, but the workspace is suspended or under review. Write to [email protected]; a new key does not help.
550 5.0.0 sending not permitted The From domain is not verified in this workspace, the envelope sender is on a different domain than the From header, or the workspace is not active.
550 5.0.0 recipient suppressed The recipient is on your suppression list after a bounce or complaint; see Bounces.
451 4.0.0 submission quota exceeded The free plan's 75 emails per UTC day or 2,000 per UTC month are used up; see Limits. It is temporary: retry after the reset or upgrade.
452 4.5.3 Maximum limit of 50 recipients reached One message can have at most 50 recipients. Split the send.
552 5.0.0 message too large The message, attachments included, is over 10 MB.
554 5.0.0 rejected by content audit Content screening found the message in violation of the Terms of Service. It is not relayed.

A 250 reply means Viesti accepted the message. Content screening runs before relay; mail it flags is held for review and shows as quarantined on the Activity page.

Next steps

  • DNS setup: DKIM, SPF, DMARC and return-path records for your domain.
  • SMTP reference: ports, TLS and authentication in detail.
  • HTTP API: send the same message as JSON over HTTPS.
  • Limits and Bounces: quotas, message size and suppressions.

Package behavior is from the net/smtp documentation. Checked 26 September 2026.