first commit

This commit is contained in:
Timofey.Kovalev
2025-10-14 10:35:42 +03:00
commit 9972005cb5
9 changed files with 386 additions and 0 deletions

15
Makefile Normal file
View File

@ -0,0 +1,15 @@
all: build/qmail
DEPENDS := go.sum \
cmd/main.go \
internal/config/*.go \
internal/mail/*.go
go.sum:
go mod tidy
build/qmail: $(DEPENDS)
CGO_ENABLED=0 go build -o build/qmail cmd/main.go
install: build/qmail
install -Dm755 build/qmail /usr/local/bin

24
README.md Normal file
View File

@ -0,0 +1,24 @@
# Qmail
The qmail utility is a tool designed for simple, fast, and convenient email sending. It allows saving settings for multiple servers in a configuration file and then sending content from a file or standard input with a single simple command.
## install
Install go
```bash
$ make
$ make install
```
## Example
```bash
$ qmail -s myServer ./mail.eml
```
or
```bash
$ cat "text" | qmail -s myServer
```

119
cmd/main.go Normal file
View File

@ -0,0 +1,119 @@
package main
import (
"fmt"
"os"
"qmail/internal/config"
"qmail/internal/mail"
"github.com/jessevdk/go-flags"
"github.com/olekukonko/tablewriter"
)
type appOptions struct {
Config string `short:"c" long:"config" description:"Config file"`
Server string `short:"s" long:"server" description:"SMTP server for sending"`
PrintList bool `short:"l" long:"list" description:"Print servers"`
From string `long:"from" description:"e-mail address indicated as sender"`
To []string `long:"to" description:""`
}
type helpOptions struct {
PrintHelp bool `short:"h" long:"help" description:"Show this help message"`
}
func main() {
appOpt := appOptions{
Config: config.DefaultConfigPath,
}
f := flags.NewParser(&appOpt, flags.PassDoubleDash)
f.Usage = "[OPTIONS] [FILE]"
f.LongDescription = "The qmail utility is a tool designed for simple, fast, and convenient email sending"
helpOpt := helpOptions{}
f.AddGroup("Help options", "", &helpOpt)
args, err := f.ParseArgs(os.Args)
if err != nil {
fmt.Println(err)
return
}
if helpOpt.PrintHelp {
f.WriteHelp(os.Stderr)
return
}
cfg, err := config.LoadConfig(appOpt.Config)
if err != nil {
fmt.Println(err)
return
}
if appOpt.PrintList {
table := tablewriter.NewWriter(os.Stdout)
table.SetHeader([]string{"Name", "Host", "Port", "TLS", "Login"})
for _, s := range cfg.Servers {
login := ""
if s.Auth != nil {
login = s.Auth.Login
}
useTLS := "-"
if s.UseTLS {
useTLS = "+"
}
table.Append([]string{
s.Name,
s.Host,
fmt.Sprintf("%d", s.Port),
useTLS,
login,
})
}
table.Render()
return
}
if appOpt.Server != "" {
var server *config.Server
for _, s := range cfg.Servers {
if s.Name == appOpt.Server {
server = &s
}
}
if server == nil {
panic("omg")
}
var bodyFile string
if len(args) > 1 {
bodyFile = args[1]
} else {
bodyFile = "/dev/stdin"
}
body, err := os.ReadFile(bodyFile)
if err != nil {
panic("qq")
}
err = mail.SendMail(server, appOpt.From, appOpt.To, body)
if err != nil {
panic(err)
}
}
}

16
go.mod Normal file
View File

@ -0,0 +1,16 @@
module qmail
go 1.24.0
require (
github.com/go-yaml/yaml v2.1.0+incompatible
github.com/jessevdk/go-flags v1.6.1
github.com/olekukonko/tablewriter v0.0.5
)
require (
github.com/mattn/go-runewidth v0.0.9 // indirect
golang.org/x/sys v0.21.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
)

20
go.sum Normal file
View File

@ -0,0 +1,20 @@
github.com/go-yaml/yaml v2.1.0+incompatible h1:RYi2hDdss1u4YE7GwixGzWwVo47T8UQwnTLB6vQiq+o=
github.com/go-yaml/yaml v2.1.0+incompatible/go.mod h1:w2MrLa16VYP0jy6N7M5kHaCkaLENm+P+Tv+MfurjSw0=
github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=
github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc=
github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws=
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=

54
internal/config/config.go Normal file
View File

@ -0,0 +1,54 @@
package config
import (
"errors"
"fmt"
"os"
"path"
"strings"
"github.com/go-yaml/yaml"
)
func LoadConfig(filePath string) (*Config, error) {
cfg := Config{}
isDefaultConfig := filePath == DefaultConfigPath
if strings.HasPrefix(filePath, "~") {
if v, ok := os.LookupEnv("HOME"); ok {
filePath = strings.Replace(filePath, "~", v, 1)
} else {
return nil, fmt.Errorf("$HOME not set in env")
}
}
if _, err := os.Stat(filePath); errors.Is(err, os.ErrNotExist) {
if isDefaultConfig {
dir, _ := path.Split(filePath)
err := os.MkdirAll(dir, os.ModePerm)
if err != nil {
return nil, err
}
err = os.WriteFile(filePath, []byte(DefaultConfig), 0644)
if err != nil {
return nil, err
}
}
return &cfg, nil
}
data, err := os.ReadFile(filePath)
if err != nil {
return nil, err
}
err = yaml.Unmarshal([]byte(data), &cfg)
if err != nil {
return nil, err
}
return &cfg, nil
}

15
internal/config/const.go Normal file
View File

@ -0,0 +1,15 @@
package config
const DefaultConfigPath = "~/.config/qmail/main.yaml"
const DefaultConfig = `
servers:
# - name: s1
# host: localhost
# port: 587
# use-tls: true
# auth:
# login: login
# password: password
`

24
internal/config/struct.go Normal file
View File

@ -0,0 +1,24 @@
package config
type Auth struct {
Login string `yaml:"login"`
Password string `yaml:"password"`
}
type Server struct {
Name string `yaml:"name"`
Host string `yaml:"host"`
Port uint16 `yaml:"port"`
From string `yaml:"from"`
To string `yaml:"to"`
UseTLS bool `yaml:"use-tls"`
Auth *Auth `yaml:"auth"`
}
type Config struct {
Servers []Server
}

99
internal/mail/mail.go Normal file
View File

@ -0,0 +1,99 @@
package mail
import (
"crypto/tls"
"fmt"
"net/smtp"
"qmail/internal/config"
)
func SendMail(server *config.Server, from string, to []string, data []byte) error {
fmt.Println("Send...")
if server.Host == "" || server.Port == 0 {
return fmt.Errorf("invalide server host or port")
}
fmt.Printf("Dial:%s:%d\n", server.Host, server.Port)
// Connect to the remote SMTP server.
c, err := smtp.Dial(fmt.Sprintf("%s:%d", server.Host, server.Port))
if err != nil {
return err
}
fmt.Println("omg")
if server.UseTLS {
fmt.Println("start TLS")
err = c.StartTLS(&tls.Config{
InsecureSkipVerify: true,
})
if err != nil {
return err
}
}
if server.Auth != nil {
fmt.Println("Auth")
auth := smtp.PlainAuth("", server.Auth.Login, server.Auth.Password, server.Host)
err = c.Auth(auth)
if err != nil {
return err
}
}
if from == "" {
from = server.From
}
if from == "" {
return fmt.Errorf("From not set")
}
fmt.Printf("Set from: %s\n", from)
// Set the sender and recipient first
if err := c.Mail(from); err != nil {
return err
}
if len(to) == 0 {
to = []string{server.To}
}
if len(to) == 0 {
return fmt.Errorf("To not set")
}
for _, tt := range to {
if err := c.Rcpt(tt); err != nil {
return err
}
}
// Send the email body.
wc, err := c.Data()
if err != nil {
return err
}
fmt.Printf("Send data")
_, err = wc.Write(data)
if err != nil {
return err
}
err = wc.Close()
if err != nil {
return err
}
fmt.Println("Code connection")
// Send the QUIT command and close the connection.
err = c.Quit()
if err != nil {
return err
}
fmt.Println("Done")
return nil
}