mirror of
https://github.com/hanzoai/base.git
synced 2026-10-21 03:41:50 +00:00
# Conflicts: # core/record_field_resolver_multi_match.go # ui/dist/assets/AuthMethodsDocs-ChShXLwE.js # ui/dist/assets/AuthMethodsDocs-Cqvl0T_l.js # ui/dist/assets/AuthMethodsDocs-DIqhS7qY.js # ui/dist/assets/AuthRefreshDocs-CHakkNnn.js # ui/dist/assets/AuthRefreshDocs-DLb1qree.js # ui/dist/assets/AuthRefreshDocs-DXgrlMdk.js # ui/dist/assets/AuthWithOAuth2Docs-Bl8VRxNv.js # ui/dist/assets/AuthWithOAuth2Docs-DVBOel22.js # ui/dist/assets/AuthWithOAuth2Docs-I213UiJ7.js # ui/dist/assets/AuthWithOtpDocs-B9tL6gIN.js # ui/dist/assets/AuthWithOtpDocs-C2PPm9-Z.js # ui/dist/assets/AuthWithOtpDocs-qEMoMG3D.js # ui/dist/assets/AuthWithPasswordDocs-B8JWDbPL.js # ui/dist/assets/AuthWithPasswordDocs-BTHeYJwE.js # ui/dist/assets/AuthWithPasswordDocs-D9rqgLpO.js # ui/dist/assets/BatchApiDocs-BV4PYYXn.js # ui/dist/assets/BatchApiDocs-DXa997am.js # ui/dist/assets/BatchApiDocs-Di2Qe2dV.js # ui/dist/assets/DeleteApiDocs-C3IINRwz.js # ui/dist/assets/DeleteApiDocs-LfmVp4Uf.js # ui/dist/assets/DeleteApiDocs-SU4XdGhJ.js # ui/dist/assets/EmailChangeDocs-COx1USU5.js # ui/dist/assets/EmailChangeDocs-D-qNHT_P.js # ui/dist/assets/EmailChangeDocs-DpfXIE_V.js # ui/dist/assets/FieldsQueryParam-BYe9ZK-G.js # ui/dist/assets/FieldsQueryParam-L8vpHeF4.js # ui/dist/assets/FieldsQueryParam-z0dGQ6uY.js # ui/dist/assets/ListApiDocs-BCTT-AOz.js # ui/dist/assets/ListApiDocs-C0_GwI7k.js # ui/dist/assets/ListApiDocs-T9_aO-pW.js # ui/dist/assets/PageInstaller-B22bivaN.js # ui/dist/assets/PageInstaller-C0BBhyBx.js # ui/dist/assets/PageInstaller-IYSwJhZW.js # ui/dist/assets/PageOAuth2RedirectFailure-BjbpqlFj.js # ui/dist/assets/PageOAuth2RedirectFailure-COyNF79R.js # ui/dist/assets/PageOAuth2RedirectFailure-DPmGG9zv.js # ui/dist/assets/PageOAuth2RedirectSuccess-6cMXLFAX.js # ui/dist/assets/PageOAuth2RedirectSuccess-CdkCarGH.js # ui/dist/assets/PageOAuth2RedirectSuccess-DSEIHeQL.js # ui/dist/assets/PageRecordConfirmEmailChange-BYW-6ASg.js # ui/dist/assets/PageRecordConfirmEmailChange-Bm1CIYvq.js # ui/dist/assets/PageRecordConfirmEmailChange-BpaqOT4u.js # ui/dist/assets/PageRecordConfirmPasswordReset--wx_u2lI.js # ui/dist/assets/PageRecordConfirmPasswordReset-CoeY2Z_C.js # ui/dist/assets/PageRecordConfirmPasswordReset-Ptya5bfe.js # ui/dist/assets/PageRecordConfirmVerification-6xjF3T0O.js # ui/dist/assets/PageRecordConfirmVerification-C1UTIIJh.js # ui/dist/assets/PageRecordConfirmVerification-C4LixXvA.js # ui/dist/assets/PageSuperuserConfirmPasswordReset-19z2IFg4.js # ui/dist/assets/PageSuperuserConfirmPasswordReset-DwNi0ugQ.js # ui/dist/assets/PageSuperuserConfirmPasswordReset-SMnDTRNi.js # ui/dist/assets/PageSuperuserRequestPasswordReset-BpDW217f.js # ui/dist/assets/PageSuperuserRequestPasswordReset-DK63qE6z.js # ui/dist/assets/PageSuperuserRequestPasswordReset-bG02qLIM.js # ui/dist/assets/PasswordResetDocs-CE67mLC0.js # ui/dist/assets/PasswordResetDocs-CHAK0vma.js # ui/dist/assets/PasswordResetDocs-Dap-N4rx.js # ui/dist/assets/RealtimeApiDocs-BooTSQCb.js # ui/dist/assets/RealtimeApiDocs-CoHmYVEx.js # ui/dist/assets/RealtimeApiDocs-D7ds3fHt.js # ui/dist/assets/VerificationDocs-B42AIoCQ.js # ui/dist/assets/VerificationDocs-BS7b11zH.js # ui/dist/assets/VerificationDocs-ZhkdFVIl.js # ui/dist/assets/ViewApiDocs-BXmRLHZ1.js # ui/dist/assets/ViewApiDocs-DM5JJQjh.js # ui/dist/assets/ViewApiDocs-DRpxcuJR.js # ui/public/fonts/source-sans-pro/source-sans-pro-v18-latin_cyrillic-700.woff2 # ui/public/fonts/ubuntu-mono/ubuntu-mono-v17-cyrillic_latin-700.woff2
212 lines
5.2 KiB
Go
212 lines
5.2 KiB
Go
package mailer
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/smtp"
|
|
"strings"
|
|
|
|
"github.com/domodwyer/mailyak/v3"
|
|
"github.com/hanzoai/base/tools/hook"
|
|
"github.com/hanzoai/base/tools/security"
|
|
)
|
|
|
|
var _ Mailer = (*SMTPClient)(nil)
|
|
|
|
const (
|
|
SMTPAuthPlain = "PLAIN"
|
|
SMTPAuthLogin = "LOGIN"
|
|
)
|
|
|
|
// SMTPClient defines a SMTP mail client structure that implements
|
|
// `mailer.Mailer` interface.
|
|
type SMTPClient struct {
|
|
onSend *hook.Hook[*SendEvent]
|
|
|
|
TLS bool
|
|
Port int
|
|
Host string
|
|
Username string
|
|
Password string
|
|
|
|
// SMTP auth method to use
|
|
// (if not explicitly set, defaults to "PLAIN")
|
|
AuthMethod string
|
|
|
|
// LocalName is optional domain name used for the EHLO/HELO exchange
|
|
// (if not explicitly set, defaults to "localhost").
|
|
//
|
|
// This is required only by some SMTP servers, such as Gmail SMTP-relay.
|
|
LocalName string
|
|
}
|
|
|
|
// OnSend implements [mailer.SendInterceptor] interface.
|
|
func (c *SMTPClient) OnSend() *hook.Hook[*SendEvent] {
|
|
if c.onSend == nil {
|
|
c.onSend = &hook.Hook[*SendEvent]{}
|
|
}
|
|
return c.onSend
|
|
}
|
|
|
|
// Send implements [mailer.Mailer] interface.
|
|
func (c *SMTPClient) Send(m *Message) error {
|
|
if c.onSend != nil {
|
|
return c.onSend.Trigger(&SendEvent{Message: m}, func(e *SendEvent) error {
|
|
return c.send(e.Message)
|
|
})
|
|
}
|
|
|
|
return c.send(m)
|
|
}
|
|
|
|
func (c *SMTPClient) send(m *Message) error {
|
|
var smtpAuth smtp.Auth
|
|
if c.Username != "" || c.Password != "" {
|
|
switch c.AuthMethod {
|
|
case SMTPAuthLogin:
|
|
smtpAuth = &smtpLoginAuth{c.Username, c.Password}
|
|
default:
|
|
smtpAuth = smtp.PlainAuth("", c.Username, c.Password, c.Host)
|
|
}
|
|
}
|
|
|
|
// create mail instance
|
|
var yak *mailyak.MailYak
|
|
if c.TLS {
|
|
var tlsErr error
|
|
yak, tlsErr = mailyak.NewWithTLS(fmt.Sprintf("%s:%d", c.Host, c.Port), smtpAuth, nil)
|
|
if tlsErr != nil {
|
|
return tlsErr
|
|
}
|
|
} else {
|
|
yak = mailyak.New(fmt.Sprintf("%s:%d", c.Host, c.Port), smtpAuth)
|
|
}
|
|
|
|
if c.LocalName != "" {
|
|
yak.LocalName(c.LocalName)
|
|
}
|
|
|
|
if m.From.Name != "" {
|
|
yak.FromName(m.From.Name)
|
|
}
|
|
yak.From(m.From.Address)
|
|
yak.Subject(m.Subject)
|
|
yak.HTML().Set(m.HTML)
|
|
|
|
if m.Text == "" {
|
|
// try to generate a plain text version of the HTML
|
|
if plain, err := html2Text(m.HTML); err == nil {
|
|
yak.Plain().Set(plain)
|
|
}
|
|
} else {
|
|
yak.Plain().Set(m.Text)
|
|
}
|
|
|
|
if len(m.To) > 0 {
|
|
yak.To(addressesToStrings(m.To, true)...)
|
|
}
|
|
|
|
if len(m.Bcc) > 0 {
|
|
yak.Bcc(addressesToStrings(m.Bcc, true)...)
|
|
}
|
|
|
|
if len(m.Cc) > 0 {
|
|
yak.Cc(addressesToStrings(m.Cc, true)...)
|
|
}
|
|
|
|
// add regular attachements (if any)
|
|
for name, data := range m.Attachments {
|
|
r, mime, err := detectReaderMimeType(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
yak.AttachWithMimeType(name, r, mime)
|
|
}
|
|
|
|
// add inline attachments (if any)
|
|
for name, data := range m.InlineAttachments {
|
|
r, mime, err := detectReaderMimeType(data)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
yak.AttachInlineWithMimeType(name, r, mime)
|
|
}
|
|
|
|
// add custom headers (if any)
|
|
var hasMessageId bool
|
|
for k, v := range m.Headers {
|
|
if strings.EqualFold(k, "Message-ID") {
|
|
hasMessageId = true
|
|
}
|
|
yak.AddHeader(k, v)
|
|
}
|
|
if !hasMessageId {
|
|
// add a default message id if missing
|
|
fromParts := strings.Split(m.From.Address, "@")
|
|
if len(fromParts) == 2 {
|
|
yak.AddHeader("Message-ID", fmt.Sprintf("<%s@%s>",
|
|
security.PseudorandomString(15),
|
|
fromParts[1],
|
|
))
|
|
}
|
|
}
|
|
|
|
return yak.Send()
|
|
}
|
|
|
|
// -------------------------------------------------------------------
|
|
// AUTH LOGIN
|
|
// -------------------------------------------------------------------
|
|
|
|
var _ smtp.Auth = (*smtpLoginAuth)(nil)
|
|
|
|
// smtpLoginAuth defines an AUTH that implements the LOGIN authentication mechanism.
|
|
//
|
|
// AUTH LOGIN is obsolete[1] but some mail services like outlook requires it [2].
|
|
//
|
|
// NB!
|
|
// It will only send the credentials if the connection is using TLS or is connected to localhost.
|
|
// Otherwise authentication will fail with an error, without sending the credentials.
|
|
//
|
|
// [1]: https://github.com/golang/go/issues/40817
|
|
// [2]: https://support.microsoft.com/en-us/office/outlook-com-no-longer-supports-auth-plain-authentication-07f7d5e9-1697-465f-84d2-4513d4ff0145?ui=en-us&rs=en-us&ad=us
|
|
type smtpLoginAuth struct {
|
|
username, password string
|
|
}
|
|
|
|
// Start initializes an authentication with the server.
|
|
//
|
|
// It is part of the [smtp.Auth] interface.
|
|
func (a *smtpLoginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
|
|
// Must have TLS, or else localhost server.
|
|
// Note: If TLS is not true, then we can't trust ANYTHING in ServerInfo.
|
|
// In particular, it doesn't matter if the server advertises LOGIN auth.
|
|
// That might just be the attacker saying
|
|
// "it's ok, you can trust me with your password."
|
|
if !server.TLS && !isLocalhost(server.Name) {
|
|
return "", nil, errors.New("unencrypted connection")
|
|
}
|
|
|
|
return "LOGIN", nil, nil
|
|
}
|
|
|
|
// Next "continues" the auth process by feeding the server with the requested data.
|
|
//
|
|
// It is part of the [smtp.Auth] interface.
|
|
func (a *smtpLoginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
|
|
if more {
|
|
switch strings.ToLower(string(fromServer)) {
|
|
case "username:":
|
|
return []byte(a.username), nil
|
|
case "password:":
|
|
return []byte(a.password), nil
|
|
}
|
|
}
|
|
|
|
return nil, nil
|
|
}
|
|
|
|
func isLocalhost(name string) bool {
|
|
return name == "localhost" || name == "127.0.0.1" || name == "::1"
|
|
}
|