Skip to content

Instantly share code, notes, and snippets.

@leblanc-simon
Last active September 11, 2024 00:21
Show Gist options
  • Save leblanc-simon/6787e29621798b76fa494c1ab326205b to your computer and use it in GitHub Desktop.
Save leblanc-simon/6787e29621798b76fa494c1ab326205b to your computer and use it in GitHub Desktop.
MTA en Go passant par un SMTP paramétré dans le binaire
#!/bin/bash
function ask() {
question="$1"
answer=""
while [ "${answer}" == "" ]; do
read -p "${question}" answer
done
echo "${answer}"
}
function askOptional() {
question="$1"
read -p "${question}" answer
echo "${answer}"
}
function askSilent() {
question="$1"
answer=""
while [ "${answer}" == "" ]; do
read -s -p "${question}" answer
done
echo "${answer}"
}
smtpServer=$(ask "SMTP server : ")
smtpPort=$(ask "SMTP port : ")
smtpUsername=$(askOptional "SMTP username : ")
smtpPassword=$(askSilent "SMTP password : ")
smtpTls=$(ask "Use TLS [Y/n] : ")
smtpStartTls=$(ask "Use STARTTLS [Y/n] : ")
CGO_ENABLED=0 go build \
-ldflags "-s -w -buildid= -X 'main.version=1.0.0' -X 'main.smtpServer=${smtpServer}' -X 'main.smtpPort=${smtpPort}' -X 'main.smtpUsername=${smtpUsername}' -X 'main.smtpPassword=${smtpPassword}' -X 'main.fakeSmtpStartTls=${smtpStartTls}' -X 'main.fakeSmtpTls=${smtpTls}'" \
-trimpath \
-o gmailx \
gmailx.go
package main
import (
"crypto/rand"
"crypto/tls"
"errors"
"flag"
"fmt"
"io"
"log"
"net/mail"
"net/smtp"
"os"
"os/user"
"strings"
"time"
)
type headers []string
type Args struct {
Version bool
Verbose bool
Ignore bool
Interactif bool
Subject string
CarbonCopy string
AdditionnalHeaders headers
EmptyNotAllowed bool
BindCarbonCopy string
To []string
Message string
SendMailMode bool
}
func (i *headers) String() string {
return "Header representation"
}
func (i *headers) Set(value string) error {
*i = append(*i, value)
return nil
}
var version = "debug"
var appName = "gmailx"
var smtpServer = "127.0.0.1"
var smtpPort = "1025"
var smtpUsername = ""
var smtpPassword = ""
var fakeSmtpStartTls = "n"
var smtpStartTls = false
var fakeSmtpTls = "n"
var smtpTls = false
func showVersion() {
additionnalInformations := ""
if smtpTls {
additionnalInformations = " with TLS"
}
if smtpStartTls {
additionnalInformations = " with STARTTLS"
}
fmt.Fprintf(flag.CommandLine.Output(), "%s (%s)%s - send mail with %s\n", appName, version, additionnalInformations, smtpUsername)
}
func main() {
if fakeSmtpTls == "y" || fakeSmtpTls == "Y" {
smtpTls = true
}
if fakeSmtpStartTls == "y" || fakeSmtpStartTls == "Y" {
smtpStartTls = true
}
args := ProcessArgs()
if args.Version && len(args.To) == 0 {
showVersion()
os.Exit(0)
}
var err error
if args.SendMailMode {
err = processMailInSendMailMode(args)
} else {
err = processMail(args)
}
if err != nil {
log.Fatalf(err.Error())
}
}
func processMail(a Args) error {
from, err := getFrom()
if err != nil {
log.Panic(err)
}
c, err := getSmtpClient()
if err != nil {
log.Panic(err)
}
// To && From
if err = c.Mail(smtpUsername); err != nil {
log.Panic(err)
}
if err = c.Rcpt(strings.Join(a.To, ",")); err != nil {
log.Panic(err)
}
// Data
w, err := c.Data()
if err != nil {
log.Panic(err)
}
_, err = w.Write(getData(from, a))
if err != nil {
log.Panic(err)
}
err = w.Close()
if err != nil {
log.Panic(err)
}
c.Quit()
return err
}
func manageSendMailHeaders(a *Args) {
lines := strings.Split(a.Message, "\n")
var index = 0
for _, line := range lines {
if !strings.Contains(line, ": ") {
break
}
if line == "\n" {
break
}
index++
if strings.Contains(line, "From: ") {
continue
}
if strings.Contains(line, "To: ") {
a.To = []string{strings.ReplaceAll(line, "To: ", "")}
continue
}
if strings.Contains(line, "Subject: ") {
a.Subject = strings.ReplaceAll(line, "Subject: ", "")
continue
}
a.AdditionnalHeaders.Set(line)
}
a.Message = strings.Join(lines[index:], "\n")
}
func processMailInSendMailMode(a Args) error {
manageSendMailHeaders(&a)
from, err := getFrom()
if err != nil {
log.Panic(err)
}
c, err := getSmtpClient()
if err != nil {
log.Panic(err)
}
// To && From
if err = c.Mail(smtpUsername); err != nil {
log.Panic(err)
}
if err = c.Rcpt(strings.Join(a.To, ",")); err != nil {
log.Panic(err)
}
// Data
w, err := c.Data()
if err != nil {
log.Panic(err)
}
_, err = w.Write(getData(from, a))
if err != nil {
log.Panic(err)
}
err = w.Close()
if err != nil {
log.Panic(err)
}
c.Quit()
return err
}
func getSmtpClient() (*smtp.Client, error) {
var c *smtp.Client
var err error
if smtpTls {
tlsconfig := &tls.Config{
InsecureSkipVerify: true,
ServerName: smtpServer,
}
conn, err := tls.Dial("tcp", smtpServer+":"+smtpPort, tlsconfig)
if err != nil {
return nil, err
}
c, err = smtp.NewClient(conn, smtpServer)
if err != nil {
return nil, err
}
} else {
c, err = smtp.Dial(smtpServer + ":" + smtpPort)
if err != nil {
return nil, err
}
if smtpStartTls {
tlsconfig := &tls.Config{
InsecureSkipVerify: true,
ServerName: smtpServer,
}
hostname, _ := os.Hostname()
err = c.Hello(hostname)
if err != nil {
return nil, err
}
c.StartTLS(tlsconfig)
if err != nil {
return nil, err
}
_, ok := c.TLSConnectionState()
if !ok {
return nil, errors.New("SMTP connection isn't TLS after StartTLS")
}
}
}
if smtpUsername != "" && smtpPassword != "" {
auth := smtp.PlainAuth("", smtpUsername, smtpPassword, smtpServer)
if err := c.Auth(auth); err != nil {
return nil, err
}
}
return c, nil
}
func getFrom() (mail.Address, error) {
currentUser, err := user.Current()
if err != nil {
return mail.Address{}, err
}
hostname, err := os.Hostname()
if err != nil {
return mail.Address{}, err
}
from := mail.Address{Name: "", Address: currentUser.Username + "@" + hostname}
if currentUser.Name != "" {
from.Name = currentUser.Name + " " + hostname
}
return from, nil
}
func getMessageId() string {
buf := make([]byte, 5)
rand.Read(buf)
hostname, _ := os.Hostname()
return fmt.Sprintf("<%s-%x@%s>", time.Now().Format("2006-01-02T15:04:05"), buf, hostname)
}
func getData(from mail.Address, a Args) []byte {
headers := make(map[string]string)
fakeFrom := mail.Address{Name: from.Name, Address: smtpUsername}
messageId := getMessageId()
headers["X-ID"] = messageId
headers["Message-Id"] = messageId
headers["From"] = fakeFrom.String()
headers["Reply-To"] = from.String()
headers["To"] = strings.Join(a.To, ",")
headers["Subject"] = a.Subject
headers["Date"] = time.Now().Format("Mon, 02 Jan 2006 15:04:05 -0700")
headers["X-Sent-With"] = fmt.Sprintf("%s (%s)", appName, version)
for _, header := range a.AdditionnalHeaders {
headerName, headerValue, found := strings.Cut(header, ":")
if found {
headers[headerName] = strings.TrimSpace(headerValue)
}
}
message := ""
for k, v := range headers {
value := strings.ReplaceAll(v, "\n", "")
value = strings.ReplaceAll(v, "\r", "")
message += fmt.Sprintf("%s: %s\r\n", k, value)
}
message += "\r\n" + a.Message
return []byte(message)
}
func ProcessArgs() Args {
var a Args
f := flag.NewFlagSet("gmailx", 1)
f.BoolVar(&a.Verbose, "v", false, "Mode verbose. Les détails de la livraison sont affichés sur le terminal de l'utilisateur.")
f.BoolVar(&a.Ignore, "i", false, "Ignore les signaux d'interruption tty. C'est particulièrement utile lors de l'utilisation de mail sur des lignes téléphoniques à bruit.")
f.BoolVar(&a.Interactif, "I", false, "Force mail à se lancer en mode interactif, même lorsque l'entrée n'est pas un terminal.")
f.StringVar(&a.Subject, "s", "", "Spécifie le sujet en ligne de commande (seul le premier argument après le flag -s est utilisé en tant que sujet ; pensez à mettre des guillemets autour des sujets contenant des espaces).")
f.StringVar(&a.CarbonCopy, "c", "", "Envoie des copies carbones à la liste d'utilisateurs. liste doit être une liste de noms séparés par des virgules.")
f.Var(&a.AdditionnalHeaders, "a", "Spécifie des champs d'en-tête additionels dans la ligne de commande comme « X-Loop: foo@bar », etc. Vous devez utiliser des guillemets si la chaîne contient des espaces. Cet argument peut être spécifié plus d'une fois, les en-têtes étant dans ce cas concaténés.")
f.BoolVar(&a.EmptyNotAllowed, "e", false, "N'envoie pas de courriers vides. Si le corps est vide, le message est sauté.")
f.StringVar(&a.BindCarbonCopy, "b", "", "Envoie des copies carbones invisibles (blind carbon copy) à liste")
f.BoolVar(&a.SendMailMode, "t", false, "Mode sendmail")
fu := f.Usage
f.Usage = func() {
showVersion()
fu()
fmt.Fprintln(f.Output())
}
f.Parse(os.Args[1:])
a.To = f.Args()
stdin, err := io.ReadAll(os.Stdin)
if err != nil {
log.Fatalf(err.Error())
}
a.Message = string(stdin)
return a
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment