From 68de8f6b270d199031061de093d14e9ac5f45a8b Mon Sep 17 00:00:00 2001 From: jinnrry Date: Fri, 10 Nov 2023 11:06:02 +0800 Subject: [PATCH] =?UTF-8?q?smtp=E5=8D=8F=E8=AE=AE=E5=AE=8C=E4=BA=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- server/config/config.go | 5 +++ server/dto/parsemail/email.go | 24 +++++++++-- server/dto/parsemail/email_decode_test.go | 6 +-- server/pop3_server/action.go | 11 +++++ server/smtp_server/read_content.go | 46 +++++++++++++++------ server/smtp_server/smtp.go | 49 ++++++++++++++++++++++- 6 files changed, 121 insertions(+), 20 deletions(-) diff --git a/server/config/config.go b/server/config/config.go index c8c732f..5c12f3c 100644 --- a/server/config/config.go +++ b/server/config/config.go @@ -13,6 +13,7 @@ var IsInit bool type Config struct { LogLevel string `json:"logLevel"` // 日志级别 Domain string `json:"domain"` + Domains []string `json:"domains"` //多域名设置,把所有收信域名都填进去 WebDomain string `json:"webDomain"` DkimPrivateKeyPath string `json:"dkimPrivateKeyPath"` SSLType string `json:"sslType"` // 0表示自动生成证书,1表示用户上传证书 @@ -71,6 +72,10 @@ func Init() { return } + if len(Instance.Domains) == 0 && Instance.Domain != "" { + Instance.Domains = []string{Instance.Domain} + } + // 读取表设置 Instance.Tables = map[string]string{} Instance.TablesInitData = map[string]string{} diff --git a/server/dto/parsemail/email.go b/server/dto/parsemail/email.go index 433281c..73f4214 100644 --- a/server/dto/parsemail/email.go +++ b/server/dto/parsemail/email.go @@ -20,6 +20,15 @@ type User struct { Name string `json:"Name"` } +func (u User) GetDomain() string { + infos := strings.Split(u.EmailAddress, "@") + if len(infos) > 2 { + return infos[1] + } + + return "" +} + type Attachment struct { Filename string ContentType string @@ -47,15 +56,24 @@ type Email struct { GroupId int // 分组id } -func NewEmailFromReader(r io.Reader) *Email { +func NewEmailFromReader(from string, to []string, r io.Reader) *Email { ret := &Email{} m, err := message.Read(r) if err != nil { log.Errorf("email解析错误! Error %+v", err) } + if from != "" { + ret.From = buildUser(from) + } else { + ret.From = buildUser(m.Header.Get("From")) + } + + if len(to) > 0 { + ret.To = buildUsers(to) + } else { + ret.To = buildUsers(m.Header.Values("To")) + } - ret.From = buildUser(m.Header.Get("From")) - ret.To = buildUsers(m.Header.Values("To")) ret.Cc = buildUsers(m.Header.Values("Cc")) ret.ReplyTo = buildUsers(m.Header.Values("ReplyTo")) ret.Sender = buildUser(m.Header.Get("Sender")) diff --git a/server/dto/parsemail/email_decode_test.go b/server/dto/parsemail/email_decode_test.go index bdce91e..e88507b 100644 --- a/server/dto/parsemail/email_decode_test.go +++ b/server/dto/parsemail/email_decode_test.go @@ -13,7 +13,7 @@ func TestDecodeEmailContentFromTxt(t *testing.T) { r := strings.NewReader(string(c)) - email := NewEmailFromReader(r) + email := NewEmailFromReader(nil, r) fmt.Println(email) } @@ -24,7 +24,7 @@ func TestDecodeEmailContentFromTxt3(t *testing.T) { r := strings.NewReader(string(c)) - email := NewEmailFromReader(r) + email := NewEmailFromReader(nil, r) fmt.Println(email) } @@ -34,7 +34,7 @@ func TestDecodeEmailContentFromTxt2(t *testing.T) { r := strings.NewReader(string(c)) - email := NewEmailFromReader(r) + email := NewEmailFromReader(nil, r) fmt.Println(email) diff --git a/server/pop3_server/action.go b/server/pop3_server/action.go index 46760ad..6ebfb94 100644 --- a/server/pop3_server/action.go +++ b/server/pop3_server/action.go @@ -25,6 +25,8 @@ func (a action) User(ctx *gopop.Data, username string) error { ctx.Ctx = tc } + log.WithContext(ctx.Ctx).Debugf("POP3 User %s", username) + ctx.User = username return nil } @@ -36,6 +38,8 @@ func (a action) Pass(ctx *gopop.Data, pwd string) error { ctx.Ctx = tc } + log.WithContext(ctx.Ctx).Debugf("POP3 PASS %s", pwd) + var user models.User encodePwd := password.Encode(pwd) @@ -65,6 +69,8 @@ func (a action) Apop(ctx *gopop.Data, username, digest string) error { ctx.Ctx = tc } + log.WithContext(ctx.Ctx).Debugf("POP3 APOP %s %s", username, digest) + var user models.User err := db.Instance.Get(&user, db.WithContext(ctx.Ctx.(*context.Context), "select * from user where account =? "), username) @@ -93,18 +99,23 @@ type statInfo struct { } func (a action) Stat(ctx *gopop.Data) (msgNum, msgSize int64, err error) { + var si statInfo err = db.Instance.Get(&si, db.WithContext(ctx.Ctx.(*context.Context), "select count(1) as `num`, sum(length(text)+length(html)) as `size` from email")) if err != nil && !errors.Is(err, sql.ErrNoRows) { log.WithContext(ctx.Ctx.(*context.Context)).Errorf("%+v", err) err = nil + log.WithContext(ctx.Ctx).Debugf("POP3 STAT RETURT :0,0") return 0, 0, nil } + log.WithContext(ctx.Ctx).Debugf("POP3 STAT RETURT : %d,%d", si.Num, si.Size) return si.Num, si.Size, nil } func (a action) Uidl(ctx *gopop.Data, id int64) (string, error) { + log.WithContext(ctx.Ctx).Debugf("POP3 Uidl RETURT : %d", id) + return cast.ToString(id), nil } diff --git a/server/smtp_server/read_content.go b/server/smtp_server/read_content.go index 1f86dd2..5d8bbc7 100644 --- a/server/smtp_server/read_content.go +++ b/server/smtp_server/read_content.go @@ -13,16 +13,18 @@ import ( "pmail/dto/parsemail" "pmail/hooks" "pmail/services/rule" + "pmail/utils/array" "pmail/utils/async" "pmail/utils/context" - "pmail/utils/id" + "pmail/utils/send" "strings" "time" ) func (s *Session) Data(r io.Reader) error { - ctx := &context.Context{} - ctx.SetValue(context.LogID, id.GenLogID()) + + ctx := s.Ctx + log.WithContext(ctx).Debugf("收到邮件") emailData, err := io.ReadAll(r) @@ -44,19 +46,38 @@ func (s *Session) Data(r io.Reader) error { log.WithContext(ctx).Infof("邮件原始内容: %s", emailData) - var dkimStatus, SPFStatus bool + email := parsemail.NewEmailFromReader(s.From, s.To, bytes.NewReader(emailData)) + // 判断是收信还是转发 + if array.InArray(email.From.GetDomain(), config.Instance.Domains) { + // 转发 + err := saveEmail(ctx, email, 1, true, true) + if err != nil { + log.WithContext(ctx).Errorf("Email Save Error %v", err) + } + + send.Send(ctx, email) - // DKIM校验 - dkimStatus = parsemail.Check(bytes.NewReader(emailData)) + } else { + // 收件 - email := parsemail.NewEmailFromReader(bytes.NewReader(emailData)) + var dkimStatus, SPFStatus bool - if err != nil { - log.WithContext(ctx).Errorf("邮件内容解析失败! Error : %v \n", err) + // DKIM校验 + dkimStatus = parsemail.Check(bytes.NewReader(emailData)) + + if err != nil { + log.WithContext(ctx).Errorf("邮件内容解析失败! Error : %v \n", err) + } + + SPFStatus = spfCheck(s.RemoteAddress.String(), email.Sender, email.Sender.EmailAddress) + + saveEmail(ctx, email, 0, SPFStatus, dkimStatus) } - SPFStatus = spfCheck(s.RemoteAddress.String(), email.Sender, email.Sender.EmailAddress) + return nil +} +func saveEmail(ctx *context.Context, email *parsemail.Email, emailType int, SPFStatus, dkimStatus bool) error { var dkimV, spfV int8 if dkimStatus { dkimV = 1 @@ -102,8 +123,9 @@ func (s *Session) Data(r io.Reader) error { return nil } - sql := "INSERT INTO email (send_date, subject, reply_to, from_name, from_address, `to`, bcc, cc, text, html, sender, attachments,spf_check, dkim_check, create_time,is_read,status,group_id) VALUES (?,?,?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" - _, err = db.Instance.Exec(sql, + sql := "INSERT INTO email (type, send_date, subject, reply_to, from_name, from_address, `to`, bcc, cc, text, html, sender, attachments,spf_check, dkim_check, create_time,is_read,status,group_id) VALUES (?,?,?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + _, err := db.Instance.Exec(sql, + emailType, email.Date, email.Subject, json2string(email.ReplyTo), diff --git a/server/smtp_server/smtp.go b/server/smtp_server/smtp.go index c313709..67e0716 100644 --- a/server/smtp_server/smtp.go +++ b/server/smtp_server/smtp.go @@ -2,10 +2,18 @@ package smtp_server import ( "crypto/tls" + "database/sql" "github.com/emersion/go-smtp" log "github.com/sirupsen/logrus" "net" "pmail/config" + "pmail/db" + "pmail/models" + "pmail/utils/context" + "pmail/utils/errors" + "pmail/utils/id" + "pmail/utils/password" + "strings" "time" ) @@ -13,27 +21,63 @@ import ( type Backend struct{} func (bkd *Backend) NewSession(conn *smtp.Conn) (smtp.Session, error) { + remoteAddress := conn.Conn().RemoteAddr() + ctx := &context.Context{} + ctx.SetValue(context.LogID, id.GenLogID()) + log.WithContext(ctx).Debugf("新SMTP连接") return &Session{ RemoteAddress: remoteAddress, + Ctx: ctx, }, nil } // A Session is returned after EHLO. type Session struct { RemoteAddress net.Addr + User string + From string + To []string + Ctx *context.Context } -func (s *Session) AuthPlain(username, password string) error { - return nil +func (s *Session) AuthPlain(username, pwd string) error { + s.User = username + + var user models.User + + encodePwd := password.Encode(pwd) + + infos := strings.Split(username, "@") + if len(infos) > 1 { + username = infos[0] + } + + err := db.Instance.Get(&user, db.WithContext(s.Ctx, "select * from user where account =? and password =?"), + username, encodePwd) + if err != nil && err != sql.ErrNoRows { + log.Errorf("%+v", err) + } + + if user.ID > 0 { + s.Ctx.UserAccount = user.Account + s.Ctx.UserID = user.ID + s.Ctx.UserName = user.Name + return nil + } + + log.WithContext(s.Ctx).Debugf("登陆错误%s %s", username, pwd) + return errors.New("password error") } func (s *Session) Mail(from string, opts *smtp.MailOptions) error { + s.From = from return nil } func (s *Session) Rcpt(to string) error { + s.To = append(s.To, to) return nil } @@ -53,6 +97,7 @@ func Start() { instance.Addr = ":25" instance.Domain = config.Instance.Domain instance.ReadTimeout = 10 * time.Second + instance.AuthDisabled = false instance.WriteTimeout = 10 * time.Second instance.MaxMessageBytes = 1024 * 1024 instance.MaxRecipients = 50