Last active
September 11, 2024 00:21
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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