1
0
Fork 0
mailgo/main.go
2026-03-31 23:14:49 +02:00

198 lines
4.4 KiB
Go

// Copyright (C) 2026 Marius Schellenberger
package main
import (
"errors"
"flag"
"fmt"
"log/slog"
"net/http"
"net/smtp"
"os"
"strings"
"git.giftfish.de/ston1th/godrop/v2"
"github.com/go-logr/logr"
)
var (
version string
listen string
addr string
domain string
log logr.Logger
)
var allowedFiles = []string{
"/etc/resolv.conf",
"/etc/localtime",
"/etc/hosts",
"/etc/ssl/cert.pem",
}
func fatal(err error) {
log.Error(err, "fatal error")
os.Exit(1)
}
func main() {
flag.StringVar(&listen, "listen", ":7979", "listen addr")
flag.StringVar(&addr, "addr", "", "smtp server addr")
flag.StringVar(&domain, "domain", "", "base sender domain (example: example.com)")
flag.Parse()
log = logr.FromSlogHandler(slog.NewTextHandler(os.Stdout, nil))
log.Info("starting mailgo", "version", version)
if addr == "" {
fatal(errors.New("missing smtp server addr"))
}
if domain == "" {
fatal(errors.New("missing base sender domain"))
}
log.Info("config",
"listen", listen,
"addr", addr,
"domain", domain,
)
if err := godrop.PledgePromises("stdio inet rpath unveil"); err != nil {
fatal(err)
}
for _, file := range allowedFiles {
if err := godrop.Unveil(file, "r"); err != nil {
fatal(err)
}
}
if err := godrop.UnveilBlock(); err != nil {
fatal(err)
}
mc := &mailClient{
server: addr,
domain: domain,
}
http.HandleFunc("/send", func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Redirect(w, r, "/?err=method", http.StatusFound)
return
}
log := log.WithName("send")
err := r.ParseForm()
if err != nil {
http.Redirect(w, r, "/?err=parse", http.StatusFound)
return
}
from := r.PostForm.Get("from")
to := r.PostForm.Get("to")
subject := r.PostForm.Get("subject")
if from == "" ||
to == "" ||
subject == "" {
http.Redirect(w, r, "/?err=missing", http.StatusFound)
return
}
if !strings.ContainsAny(to, "@") {
http.Redirect(w, r, "/?err=invalid", http.StatusFound)
return
}
log = log.WithValues("to", to, "from", from, "subject", subject)
log.Info("sending mail")
msg := r.PostForm.Get("message")
err = mc.sendMail(from, to, subject, msg)
if err != nil {
http.Redirect(w, r, "/?err=send", http.StatusFound)
log.Error(err, "mail send failed")
return
}
http.Redirect(w, r, "/", http.StatusFound)
})
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(page))
})
err := http.ListenAndServe(listen, nil)
if err != nil {
fatal(err)
}
}
type mailClient struct {
server string
domain string
}
const msgTmpl = "From: %s\r\n" +
"To: %s\r\n" +
"Subject: %s\r\n\r\n" +
"%s\r\n"
func (mc *mailClient) sendMail(from, to, subj, msg string) error {
from += "@" + mc.domain
message := fmt.Sprintf(msgTmpl, from, to, subj, msg)
return smtp.SendMail(mc.server, nil, from, []string{to}, []byte(message))
}
const page = `<!doctype html><html lang="en">
<head>
<title>MailGo</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#009670">
<meta name="description" content="MailGo">
<style>
body {
font: 20px Helvetica, sans-serif;
background-color: #303030;
color: #e6e6e6;
margin: 0px;
padding: 0 150px 0 150px;
}
@media only screen and (max-width: 800px) {
body {
padding: 0;
}
}
div {
margin-bottom: 10px;
}
label {
display: inline-block;
width: 100px;
text-align: left;
}
input, textarea {
color: #e2e2e2;
background-color: #444444;
border-radius: 0.375rem;
border: 1px solid #222;
font-size: 1rem;
font-weight: 400;
}
textarea {
width: 80%;
}
h3 {
background-color: #009670;
margin: 0 0 20px 0;
padding: 10px 0 10px 0;
text-align: center;
color: #fff;
}
</style>
</head>
<body>
<h3>MailGo</h3>
<center>
<form action="/send" method="post" autocomplete="off">
<div><label for="from">From:</label></div>
<div><input type="text" id="from" name="from" required autofocus></input></div>
<div><label for="to">To:</label></div>
<div><input type="text" id="to" name="to" required></input></div>
<div><label for="subject">Subject:</label></div>
<div><input type="text" id="subject" name="subject" required></input></div>
<div><label for="message">Message:</label></div>
<div><textarea id="message" name="message" rows="10" spellcheck="false"></textarea></div>
<input type="submit" style="font-size:1em;" value="Send"></input>
</form>
</center></body></html>`