273 lines
6.8 KiB
Go
273 lines
6.8 KiB
Go
package smtpserver
|
|
|
|
import (
|
|
"b612.me/apps/b612/utils"
|
|
"b612.me/starlog"
|
|
"b612.me/startext"
|
|
"bytes"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"fmt"
|
|
"github.com/emersion/go-sasl"
|
|
"github.com/spf13/cobra"
|
|
"html"
|
|
"io"
|
|
"io/ioutil"
|
|
"mime"
|
|
"mime/quotedprintable"
|
|
"net/mail"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/emersion/go-smtp"
|
|
)
|
|
|
|
var addr string
|
|
var user, pass string
|
|
var allowAnyuser, startls bool
|
|
var output string
|
|
var domain string
|
|
var cert, key string
|
|
var allowInsecureAuth, autoGenKey bool
|
|
|
|
var Cmd = &cobra.Command{
|
|
Use: "smtps",
|
|
Short: "smtp server",
|
|
Long: "smtp server",
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
run()
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
Cmd.Flags().StringVarP(&addr, "addr", "a", "0.0.0.0:25", "smtp server listen address")
|
|
Cmd.Flags().StringVarP(&user, "user", "u", "admin", "smtp server username")
|
|
Cmd.Flags().StringVarP(&pass, "pass", "p", "admin", "smtp server password")
|
|
Cmd.Flags().BoolVarP(&allowAnyuser, "allow-anyuser", "A", false, "allow any user")
|
|
Cmd.Flags().StringVarP(&output, "output", "o", "", "output mail to html")
|
|
Cmd.Flags().StringVarP(&domain, "domain", "d", "localhost", "smtp server domain")
|
|
Cmd.Flags().StringVarP(&cert, "cert", "c", "", "smtp server cert(TLS)")
|
|
Cmd.Flags().StringVarP(&key, "key", "k", "", "smtp server key(TLS)")
|
|
Cmd.Flags().BoolVarP(&autoGenKey, "auto-gen-key", "g", false, "auto generate key and cert")
|
|
Cmd.Flags().BoolVarP(&startls, "startls", "s", false, "enable starttls")
|
|
Cmd.Flags().BoolVarP(&allowInsecureAuth, "allow-insecure-auth", "i", false, "allow insecure auth")
|
|
}
|
|
|
|
type backend struct{}
|
|
|
|
func (bkd *backend) NewSession(c *smtp.Conn) (smtp.Session, error) {
|
|
return &session{}, nil
|
|
}
|
|
|
|
type session struct {
|
|
username string
|
|
password string
|
|
to string
|
|
auth bool
|
|
}
|
|
|
|
func (s *session) AuthMechanisms() []string {
|
|
return []string{sasl.Plain}
|
|
}
|
|
|
|
func (s *session) Auth(mech string) (sasl.Server, error) {
|
|
return sasl.NewPlainServer(func(identity, username, password string) error {
|
|
s.username = username
|
|
s.password = password
|
|
starlog.Red("username:%s,password:%s\n", username, password)
|
|
if allowAnyuser {
|
|
s.auth = true
|
|
return nil
|
|
} else {
|
|
if username != user || password != pass {
|
|
return smtp.ErrAuthFailed
|
|
}
|
|
}
|
|
s.auth = true
|
|
return nil
|
|
}), nil
|
|
}
|
|
|
|
func (s *session) Mail(from string, opts *smtp.MailOptions) error {
|
|
if !s.auth {
|
|
return smtp.ErrAuthRequired
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *session) Rcpt(to string, opts *smtp.RcptOptions) error {
|
|
if !s.auth {
|
|
return smtp.ErrAuthRequired
|
|
}
|
|
s.to += to + ";"
|
|
return nil
|
|
}
|
|
|
|
func (s *session) Data(r io.Reader) error {
|
|
if !s.auth {
|
|
return smtp.ErrAuthRequired
|
|
}
|
|
mailData, err := ioutil.ReadAll(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
msg, err := mail.ReadMessage(bytes.NewReader(mailData))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
header := msg.Header
|
|
subject := header.Get("Subject")
|
|
to := header.Get("To")
|
|
cc := header.Get("Cc")
|
|
from := header.Get("From") // 获取发件人
|
|
date := header.Get("Date")
|
|
body, err := ioutil.ReadAll(msg.Body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var bodyStr string
|
|
|
|
var d = new(mime.WordDecoder)
|
|
{
|
|
subject, err = d.DecodeHeader(subject)
|
|
if err != nil {
|
|
starlog.Errorf("Decode subject %s error:%s\n", subject, err)
|
|
return err
|
|
}
|
|
bodyStr, err = bodyDecode(string(body))
|
|
if err != nil {
|
|
starlog.Errorf("Decode body %s error:%s\n", string(body), err)
|
|
return err
|
|
}
|
|
}
|
|
if startext.IsGBK([]byte(bodyStr)) {
|
|
tmp, err := startext.GBK2UTF8([]byte(bodyStr))
|
|
if err == nil {
|
|
bodyStr = string(tmp)
|
|
}
|
|
}
|
|
starlog.Println("From:", from)
|
|
starlog.Println("Subject:", subject)
|
|
starlog.Println("To ALL:", s.to)
|
|
starlog.Println("To:", to)
|
|
starlog.Println("Cc:", cc)
|
|
starlog.Println("Body:", bodyStr)
|
|
if output != "" {
|
|
path := fmt.Sprintf("%s/%s_%s.html", output, subject, time.Now().Format("2006_01_02_15_04_05_"))
|
|
html := fmt.Sprintf(`<html><head>
|
|
<meta charset="utf-8">
|
|
<title>%s</title>
|
|
</head>
|
|
<body>
|
|
<h2>%s</h2>
|
|
<hr>
|
|
<p>auth user:<strong> %s </strong></p>
|
|
<p>auth pass:<strong> %s </strong></p>
|
|
<hr>
|
|
<p>Date:<strong> %v </strong></p>
|
|
<p>From:<strong> %s </strong></p>
|
|
<p>To All:<strong> %s </strong></p>
|
|
<p>To:<strong> %s </strong></p>
|
|
<p>Cc:<strong> %s </strong></p>
|
|
<hr>
|
|
<br />
|
|
<br />
|
|
%s
|
|
</body>
|
|
</html>`, html.EscapeString(subject), html.EscapeString(subject), html.EscapeString(s.username), html.EscapeString(s.password),
|
|
date, html.EscapeString(from), html.EscapeString(s.to), html.EscapeString(to), html.EscapeString(cc), bodyStr)
|
|
os.WriteFile(path, []byte(html), 0644)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *session) Reset() {}
|
|
|
|
func (s *session) Logout() error {
|
|
return nil
|
|
}
|
|
|
|
func run() {
|
|
var err error
|
|
s := smtp.NewServer(&backend{})
|
|
s.Addr = addr
|
|
s.Domain = domain
|
|
s.EnableSMTPUTF8 = true
|
|
s.EnableDSN = true
|
|
s.EnableBINARYMIME = true
|
|
s.EnableRRVS = true
|
|
if autoGenKey || (cert != "" && key != "") {
|
|
var config tls.Config
|
|
config.Certificates = make([]tls.Certificate, 1)
|
|
if !autoGenKey {
|
|
config.Certificates[0], err = tls.LoadX509KeyPair(cert, key)
|
|
if err != nil {
|
|
starlog.Errorln("failed to load cert:", err)
|
|
return
|
|
}
|
|
} else {
|
|
ca, cakey := utils.ToolCert("")
|
|
genCert, err := utils.GenerateTlsCert(utils.GenerateCertParams{
|
|
Country: "CN",
|
|
Organization: "B612 SMTP SERVER",
|
|
OrganizationUnit: "CA@B612.ME",
|
|
CommonName: s.Domain,
|
|
Dns: []string{s.Domain},
|
|
KeyUsage: int(x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageCertSign),
|
|
ExtendedKeyUsage: []int{
|
|
int(x509.ExtKeyUsageServerAuth),
|
|
int(x509.ExtKeyUsageClientAuth),
|
|
},
|
|
IsCA: false,
|
|
StartDate: time.Now().Add(-24 * time.Hour),
|
|
EndDate: time.Now().AddDate(1, 0, 0),
|
|
Type: "RSA",
|
|
Bits: 2048,
|
|
CA: ca,
|
|
CAPriv: cakey,
|
|
})
|
|
if err != nil {
|
|
starlog.Errorln("failed to generate cert:", err)
|
|
return
|
|
}
|
|
config.Certificates[0] = genCert
|
|
}
|
|
s.TLSConfig = &config
|
|
s.AllowInsecureAuth = allowInsecureAuth
|
|
s.EnableREQUIRETLS = true
|
|
s.Debug = os.Stdout
|
|
if !startls {
|
|
starlog.Infoln("Starting TLS-SMTP server at", addr)
|
|
starlog.Errorln(s.ListenAndServeTLS())
|
|
return
|
|
}
|
|
starlog.Infoln("Starting StartTls-SMTP server at", addr)
|
|
starlog.Errorln(s.ListenAndServe())
|
|
return
|
|
}
|
|
s.AllowInsecureAuth = true
|
|
s.Debug = os.Stdout
|
|
|
|
starlog.Infoln("Starting SMTP server at", addr)
|
|
starlog.Errorln(s.ListenAndServe())
|
|
}
|
|
|
|
func bodyDecode(encoded string) (string, error) {
|
|
encoded = strings.Replace(encoded, "=\r\n", "", -1) // 对于Windows系统
|
|
encoded = strings.Replace(encoded, "=\n", "", -1) // 对于UNIX/Linux系统
|
|
|
|
// 创建一个新的Quoted-Printable阅读器
|
|
reader := quotedprintable.NewReader(strings.NewReader(encoded))
|
|
|
|
// 读取并解码整个内容
|
|
decoded, err := ioutil.ReadAll(reader)
|
|
if err != nil {
|
|
fmt.Println("Error decoding string:", err)
|
|
return string(decoded), err
|
|
}
|
|
return string(decoded), nil
|
|
}
|