feat: publish standalone worker
Separate worker packaging and service lifecycle from the control plane.
Этот коммит содержится в:
219
internal/sender/email.go
Обычный файл
219
internal/sender/email.go
Обычный файл
@@ -0,0 +1,219 @@
|
||||
package sender
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/mail"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gopkg.in/gomail.v2"
|
||||
|
||||
"rsgit.ru/rsmon/rsmon/app/models"
|
||||
"rsgit.ru/rsmon/rsmon/internal/wire"
|
||||
)
|
||||
|
||||
// RunEmail provides functionality.
|
||||
func RunEmail(message *models.Message) error {
|
||||
subject, textBody, _, htmlBody := GetContent(message, time.Now())
|
||||
return SendEmail(message.Contact.Value, subject.String(), textBody.String(), htmlBody.String())
|
||||
}
|
||||
|
||||
// SendEmail provides functionality.
|
||||
func SendEmail(to, subject, body, html string) error {
|
||||
cred, err := firstSMTPCredential()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return SendEmailWithCredential(to, subject, body, html, cred)
|
||||
}
|
||||
|
||||
func firstSMTPCredential() (*wire.SMTPCredential, error) {
|
||||
creds, err := models.EnabledCredentialsByKind(models.CredentialKindSMTP)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(creds) == 0 {
|
||||
return nil, errors.New("smtp credential is not configured")
|
||||
}
|
||||
return smtpModelToWire(&creds[0])
|
||||
}
|
||||
|
||||
func smtpModelToWire(cred *models.NotificationCredential) (*wire.SMTPCredential, error) {
|
||||
if cred == nil {
|
||||
return nil, errors.New("smtp credential is nil")
|
||||
}
|
||||
password, err := cred.GetSecret()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("smtp credential secret: %w", err)
|
||||
}
|
||||
out := &wire.SMTPCredential{
|
||||
ID: cred.ID,
|
||||
Name: cred.Name,
|
||||
Password: password,
|
||||
InsecureSkipVerify: cred.InsecureSkipVerify,
|
||||
}
|
||||
if cred.Server != nil {
|
||||
out.Server = *cred.Server
|
||||
}
|
||||
if cred.Port != nil {
|
||||
out.Port = *cred.Port
|
||||
}
|
||||
if cred.Login != nil {
|
||||
out.Login = *cred.Login
|
||||
}
|
||||
if cred.FromName != nil {
|
||||
out.FromName = *cred.FromName
|
||||
}
|
||||
if cred.FromAddr != nil {
|
||||
out.FromAddress = *cred.FromAddr
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// SendEmailWithCredential delivers an email using the given wire SMTP
|
||||
// credential. Phase 1 of docs/plans/worker-notifier-mvp.md ships every
|
||||
// enabled SMTP credential to the worker; this function is what the worker
|
||||
// executor calls so the same code path covers the operated-worker path and
|
||||
// the per-customer-credential path Phase 4 will introduce.
|
||||
func SendEmailWithCredential(to, subject, body, html string, cred *wire.SMTPCredential) error {
|
||||
return SendEmailWithCredentialContext(context.Background(), to, subject, body, html, cred)
|
||||
}
|
||||
|
||||
// SendEmailWithCredentialContext performs SMTP over a context-aware dialer.
|
||||
// Socket deadlines propagate cancellation through SMTP commands and DATA writes.
|
||||
func SendEmailWithCredentialContext(ctx context.Context, to, subject, body, html string, cred *wire.SMTPCredential) error {
|
||||
return sendEmailWithCredentialContext(ctx, to, subject, body, html, cred, cred != nil && cred.Port == 465)
|
||||
}
|
||||
|
||||
// sendEmailWithCredentialContext permits the SMTPS transport choice to be
|
||||
// tested with an unprivileged local listener.
|
||||
func sendEmailWithCredentialContext(ctx context.Context, to, subject, body, html string, cred *wire.SMTPCredential, implicitTLS bool) error {
|
||||
if cred == nil {
|
||||
return errors.New("smtp credential is nil")
|
||||
}
|
||||
if cred.Server == "" {
|
||||
return errors.New("smtp credential: server is empty")
|
||||
}
|
||||
fromAddr := &mail.Address{Name: cred.FromName, Address: cred.FromAddress}
|
||||
from := fromAddr.String()
|
||||
|
||||
m := gomail.NewMessage()
|
||||
m.SetHeader("From", from)
|
||||
m.SetHeader("To", to)
|
||||
m.SetHeader("Subject", subject)
|
||||
m.AddAlternative("text/plain", body)
|
||||
m.AddAlternative("text/html", html)
|
||||
|
||||
var raw bytes.Buffer
|
||||
if _, err := m.WriteTo(&raw); err != nil {
|
||||
return err
|
||||
}
|
||||
dialer := &net.Dialer{}
|
||||
conn, err := dialer.DialContext(ctx, "tcp", net.JoinHostPort(cred.Server, fmt.Sprint(cred.Port)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close() //nolint:errcheck
|
||||
if deadline, ok := ctx.Deadline(); ok {
|
||||
_ = conn.SetDeadline(deadline)
|
||||
}
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
_ = conn.SetDeadline(time.Now())
|
||||
case <-done:
|
||||
}
|
||||
}()
|
||||
if implicitTLS {
|
||||
tlsConn := tls.Client(conn, smtpTLSConfig(cred))
|
||||
if err := tlsConn.HandshakeContext(ctx); err != nil {
|
||||
return smtpContextError(ctx, err)
|
||||
}
|
||||
conn = tlsConn
|
||||
}
|
||||
client, err := smtp.NewClient(conn, cred.Server)
|
||||
if err != nil {
|
||||
return smtpContextError(ctx, err)
|
||||
}
|
||||
defer client.Quit() //nolint:errcheck
|
||||
if ok, _ := client.Extension("STARTTLS"); ok {
|
||||
if err := client.StartTLS(smtpTLSConfig(cred)); err != nil {
|
||||
return smtpContextError(ctx, err)
|
||||
}
|
||||
}
|
||||
if cred.Login != "" {
|
||||
if err := client.Auth(smtpAuth(client, cred)); err != nil {
|
||||
return smtpContextError(ctx, err)
|
||||
}
|
||||
}
|
||||
if err := client.Mail(cred.FromAddress); err != nil {
|
||||
return smtpContextError(ctx, err)
|
||||
}
|
||||
if err := client.Rcpt(to); err != nil {
|
||||
return smtpContextError(ctx, err)
|
||||
}
|
||||
writer, err := client.Data()
|
||||
if err != nil {
|
||||
return smtpContextError(ctx, err)
|
||||
}
|
||||
if _, err := writer.Write(raw.Bytes()); err != nil {
|
||||
return smtpContextError(ctx, err)
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
return smtpContextError(ctx, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func smtpContextError(ctx context.Context, err error) error {
|
||||
if ctx.Err() != nil {
|
||||
return ctx.Err()
|
||||
}
|
||||
if deadline, ok := ctx.Deadline(); ok && !time.Now().Before(deadline) {
|
||||
return context.DeadlineExceeded
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func smtpTLSConfig(cred *wire.SMTPCredential) *tls.Config {
|
||||
return &tls.Config{ServerName: cred.Server, InsecureSkipVerify: cred.InsecureSkipVerify}
|
||||
}
|
||||
|
||||
func smtpAuth(client *smtp.Client, cred *wire.SMTPCredential) smtp.Auth {
|
||||
_, mechanisms := client.Extension("AUTH")
|
||||
for _, mechanism := range strings.Fields(strings.ToUpper(mechanisms)) {
|
||||
switch mechanism {
|
||||
case "CRAM-MD5":
|
||||
return smtp.CRAMMD5Auth(cred.Login, cred.Password)
|
||||
case "LOGIN":
|
||||
return loginAuth{username: cred.Login, password: cred.Password}
|
||||
case "PLAIN":
|
||||
return smtp.PlainAuth("", cred.Login, cred.Password, cred.Server)
|
||||
}
|
||||
}
|
||||
// Preserve gomail's default when the server does not advertise mechanisms;
|
||||
// smtp.Client returns the server's authoritative AUTH failure.
|
||||
return smtp.PlainAuth("", cred.Login, cred.Password, cred.Server)
|
||||
}
|
||||
|
||||
type loginAuth struct{ username, password string }
|
||||
|
||||
func (a loginAuth) Start(_ *smtp.ServerInfo) (string, []byte, error) { return "LOGIN", nil, nil }
|
||||
func (a loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
|
||||
if !more {
|
||||
return nil, nil
|
||||
}
|
||||
challenge := strings.ToLower(string(fromServer))
|
||||
if strings.Contains(challenge, "username") || strings.Contains(challenge, "user") {
|
||||
return []byte(a.username), nil
|
||||
}
|
||||
return []byte(a.password), nil
|
||||
}
|
||||
Ссылка в новой задаче
Block a user