Since the last update, we have added support SMTP integration for all supported frameworks (Flask, Django, FastAPI and Go)! SMTP (Simple Mail Transfer Protocol) is the technical standard for sending and receiving emails. With this new integration, we support the smtp interface, this integration is provided by the smtp-integrator charm, you can use smtp-integrator charm to configure SMTP credentials. Below is an example of how you can configure a simple Go app to integrate with the smtp-integrator charm to send an email to a local email server.
SMTP Go Application
In this post, we will see how can we charm a Go app to integrate it with the smtp-integrator charm to send email to a local SMTP server. We will deploy Mailcatcher to a Kubernetes pod and use it as the local SMTP server. We will start writing our application by creating a multipass instance:
multipass launch --cpus 4 --disk 50G --memory 4G --name charm-dev 24.04
multipass shell charm-dev
sudo snap install rockcraft --channel latest/edge --classic
sudo snap install lxd lxd init --auto
sudo snap install charmcraft --channel latest/edge --classic
sudo snap install microk8s --channel 1.31-strict/stable
adduser $USER snap_microk8s
newgrp snap_microk8s
sudo microk8s enable hostpath-storage
sudo microk8s enable registry
sudo microk8s enable ingress
sudo snap install juju --channel 3.6/stable
mkdir -p ~/.local/share
juju bootstrap microk8s dev-controller
juju add-model smtp-demo
Now that we have our multipass instance and Juju model lets start by deploying the mailcatcher.
To deploy the mailcatcher service in a Kubernetes pod we will write a pod specification yaml file named mailcatcher.yaml
:
---
apiVersion: v1
kind: Pod
metadata:
name: mailcatcher
namespace: smtp-demo
labels:
app.kubernetes.io/name: mailcatcher
spec:
containers:
- name: mailcatcher
image: sj26/mailcatcher
ports:
- containerPort: 1025
- containerPort: 1080
---
kind: Service
apiVersion: v1
metadata:
name: mailcatcher-service-9
namespace: smtp-demo
spec:
type: ClusterIP
ports:
- name: tcp-1025
port: 1025
protocol: TCP
targetPort: 1025
- name: tcp-1080
port: 1080
protocol: TCP
targetPort: 1080
selector:
app.kubernetes.io/name: mailcatcher
After we have created the mailcatcher.yaml
file we can run the following command to deploy it:
kubectl apply -f mailcatcher.yaml
Now that we have the mailcatcher pod deploying in the background, we can start writing the Go application.
First, create the folder for our application:
mkdir go-smtp
cd go-smtp
Before we can start writing the Go application, we need to install the Go package. We can easily to that with snap
:
sudo snap install go --classic
After we installed Go we can initialize the go-smtp
package:
go mod init go-smtp
Let’s create the main.go file with the following contents:
// Copyright 2025 Canonical Ltd.
// See LICENSE file for licensing details.
package main
import (
"context"
"crypto/tls"
"errors"
"fmt"
"log"
"net/mail"
"net/smtp"
"os"
"os/signal"
"syscall"
"time"
"encoding/json"
"net/http"
)
func serveHelloWorld(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
}
func handleError(w http.ResponseWriter, error_message error) {
w.WriteHeader(http.StatusInternalServerError)
w.Header().Set("Content-Type", "application/json")
resp := make(map[string]string)
resp["message"] = error_message.Error()
jsonResp, err := json.Marshal(resp)
if err != nil {
log.Fatalf("Error happened in JSON marshal. Err: %s", err)
}
w.Write(jsonResp)
}
func serveMailHandler(w http.ResponseWriter, r *http.Request) {
from := mail.Address{"", "tester@example.com"}
to := mail.Address{"", "test@example.com"}
subj := "hello"
body := "Hello world!"
// Setup headers
headers := make(map[string]string)
headers["From"] = from.String()
headers["To"] = to.String()
headers["Subject"] = subj
// Setup message
message := ""
for k, v := range headers {
message += fmt.Sprintf("%s: %s\r\n", k, v)
}
message += "\r\n" + body
// Connect to the SMTP Server
smtp_host, _ := os.LookupEnv("SMTP_HOST")
smtp_port, _ := os.LookupEnv("SMTP_PORT")
smtp_servername := smtp_host + ":" + smtp_port
smtp_user, _ := os.LookupEnv("SMTP_USER")
smtp_domain, _ := os.LookupEnv("SMTP_DOMAIN")
smtp_password, _ := os.LookupEnv("SMTP_PASSWORD")
auth := smtp.PlainAuth("", smtp_user+"@"+smtp_domain, smtp_password, smtp_host)
smtp_transport_security, _ := os.LookupEnv("SMTP_TRANSPORT_SECURITY")
c, err := smtp.Dial(smtp_servername)
defer c.Quit()
if err != nil {
handleError(w, err)
}
if smtp_transport_security == "starttls" {
// TLS config
tlsconfig := &tls.Config{
InsecureSkipVerify: true,
ServerName: smtp_host,
}
c.StartTLS(tlsconfig)
}
// Auth
if smtp_transport_security == "tls" {
if err = c.Auth(auth); err != nil {
handleError(w, err)
}
}
// To && From
if err = c.Mail(from.Address); err != nil {
handleError(w, err)
}
if err = c.Rcpt(to.Address); err != nil {
handleError(w, err)
}
// Data
m, err := c.Data()
if err != nil {
handleError(w, err)
}
_, err = m.Write([]byte(message))
if err != nil {
handleError(w, err)
}
err = m.Close()
if err != nil {
handleError(w, err)
}
fmt.Fprintf(w, "Sent")
}
func main() {
port, found := os.LookupEnv("APP_PORT")
if !found {
port = "8080"
}
mux := http.NewServeMux()
mux.HandleFunc("/", serveHelloWorld)
mux.HandleFunc("/send_mail", serveMailHandler)
server := &http.Server{
Addr: ":" + port,
Handler: mux,
}
go func() {
if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("HTTP server error: %v", err)
}
log.Println("Stopped serving new connections.")
}()
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), 10*time.Second)
defer shutdownRelease()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Fatalf("HTTP shutdown error: %v", err)
}
log.Println("Graceful shutdown complete.")
}
Now we need to create the OCI image and charm for the Go application. Then we can deploy it and use it to send emails to our local SMTP server:
export ROCKCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=True
rockcraft init --profile go-framework --name go-smtp
rockcraft pack
rockcraft.skopeo --insecure-policy copy --dest-tls-verify=false oci-archive:go-smtp_0.1_amd64.rock docker://localhost:32000/go-smtp:0.1
mkdir charm
cd charm
charmcraft init --profile go-framework --name go-smtp
We need to modify the charmcraft.yaml
file to add the SMTP integration. Add the following lines to the end of the file:
requires:
smtp:
interface: smtp
optional: false
limit: 1
We can continue with charming:
# Pack the charm
export CHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=True
charmcraft fetch-libs
charmcraft pack
# Deploy the charm into go-smtp model
juju deploy ./go-smtp_amd64.charm --resource app-image=localhost:32000/go-smtp:0.1
# Get the mailcatcher ip
MAILCATCHER_IP=$(kubectl get pod mailcatcher -o wide -n smtp-demo -o yaml | yq '.status.podIP')
# Deploy smtp-integrator charm and integrate with the Go app
juju deploy smtp-integrator --channel latest/edge --config auth_type=none --config domain=example.com --config host=$MAILCATCHER_IP --config port=1025
juju integrate go-smtp:smtp smtp-integrator:smtp
Now that our charm is deployed, we can send a request to the Go app to send an email:
# Get the go-smtp container ip
UNIT_IP=$(juju show-unit go-smtp/0 | yq '.go-smtp/0.address')
# Get the ip address of go-smtp container
curl $UNIT_IP:8080/send_mail
To check if the mailcatcher service has received the email we can curl its REST API endpoint:
# Check if mailcatcher has received the mail
curl $MAILCATCHER_IP:1080/messages
# example result:
#[{"id":1,"sender":"<tester@example.com> size=334","recipients":["<test@example.com>"],"subject":"hello","size":"336","created_at":"2025-03-17T10:56:50+00:00"}]
That’s all it takes to enable SMTP integration for our webapp! We have deployed the Go application and just integrated with the smtp-integrator charm and it all worked like a charm :).
To clean up, exit the multipass VM and run multipass delete --purge charm-dev
.