first commit
This commit is contained in:
15
Makefile
Normal file
15
Makefile
Normal 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
24
README.md
Normal 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
119
cmd/main.go
Normal 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
16
go.mod
Normal 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
20
go.sum
Normal 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
54
internal/config/config.go
Normal 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
15
internal/config/const.go
Normal 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
24
internal/config/struct.go
Normal 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
99
internal/mail/mail.go
Normal 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user