Files
root 44406b72db Initial commit: img-infinite-canvas AI design workbench MVP
Moteva-style AI design workbench replica. Home prompt creates a project;
the project page provides an infinite canvas with node drag, zoom/pan,
a design chat panel, history replay, image asset upload, and project
save/regenerate.

- Backend: go-zero, DDD layering, sqlc/pgx, optional PostgreSQL; memory
  or Redis cache; asynq job queue; MinIO/S3/R2/OSS object storage;
  sky-valley/pi agent runtime adapter.
- Frontend: Next.js App Router + Vite artifact build, TypeScript, i18n,
  shadcn/ui components, auth (OTP/Turnstile/Google/WeChat).
- Deploy: Docker Compose and k3s manifests.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 23:15:37 +08:00

140 lines
3.7 KiB
Go

package auth
import (
"bytes"
"context"
"crypto/tls"
"fmt"
"mime"
"net"
"net/mail"
"net/smtp"
"strings"
"time"
)
type SMTPOptions struct {
Host string
Port int
Username string
Password string
UseTLS bool
StartTLS bool
InsecureSkipVerify bool
Timeout time.Duration
}
type SMTPSender struct {
opts SMTPOptions
}
func NewSMTPSender(opts SMTPOptions) (*SMTPSender, error) {
opts.Host = strings.TrimSpace(opts.Host)
if opts.Host == "" {
return nil, fmt.Errorf("%w: smtp host is required", ErrProviderNotReady)
}
if opts.Port <= 0 {
opts.Port = 587
}
if opts.Timeout <= 0 {
opts.Timeout = 10 * time.Second
}
return &SMTPSender{opts: opts}, nil
}
func (s *SMTPSender) SendLoginCode(ctx context.Context, message EmailMessage) error {
if s == nil {
return ErrProviderNotReady
}
if strings.TrimSpace(message.FromEmail) == "" || strings.TrimSpace(message.ToEmail) == "" {
return fmt.Errorf("%w: email sender and recipient are required", ErrInvalidCredentials)
}
ctx, cancel := context.WithTimeout(ctx, s.opts.Timeout)
defer cancel()
client, err := s.smtpClient(ctx)
if err != nil {
return err
}
defer client.Close()
if s.opts.StartTLS && !s.opts.UseTLS {
if ok, _ := client.Extension("STARTTLS"); ok {
if err := client.StartTLS(s.tlsConfig()); err != nil {
return err
}
}
}
if strings.TrimSpace(s.opts.Username) != "" {
auth := smtp.PlainAuth("", s.opts.Username, s.opts.Password, s.opts.Host)
if err := client.Auth(auth); err != nil {
return err
}
}
if err := client.Mail(message.FromEmail); err != nil {
return err
}
if err := client.Rcpt(message.ToEmail); err != nil {
return err
}
writer, err := client.Data()
if err != nil {
return err
}
if _, err := writer.Write(buildSMTPMessage(message)); err != nil {
_ = writer.Close()
return err
}
if err := writer.Close(); err != nil {
return err
}
return client.Quit()
}
func (s *SMTPSender) smtpClient(ctx context.Context) (*smtp.Client, error) {
address := fmt.Sprintf("%s:%d", s.opts.Host, s.opts.Port)
dialer := &net.Dialer{Timeout: s.opts.Timeout}
conn, err := dialer.DialContext(ctx, "tcp", address)
if err != nil {
return nil, err
}
if s.opts.UseTLS {
tlsConn := tls.Client(conn, s.tlsConfig())
if err := tlsConn.HandshakeContext(ctx); err != nil {
_ = conn.Close()
return nil, err
}
conn = tlsConn
}
return smtp.NewClient(conn, s.opts.Host)
}
func (s *SMTPSender) tlsConfig() *tls.Config {
return &tls.Config{
ServerName: s.opts.Host,
InsecureSkipVerify: s.opts.InsecureSkipVerify,
}
}
func buildSMTPMessage(message EmailMessage) []byte {
boundary := "moteva-auth-code"
from := mail.Address{Name: message.FromName, Address: message.FromEmail}
to := mail.Address{Address: message.ToEmail}
var buffer bytes.Buffer
buffer.WriteString("From: " + from.String() + "\r\n")
buffer.WriteString("To: " + to.String() + "\r\n")
buffer.WriteString("Subject: " + mime.QEncoding.Encode("utf-8", message.Subject) + "\r\n")
buffer.WriteString("MIME-Version: 1.0\r\n")
buffer.WriteString("Content-Type: multipart/alternative; boundary=" + boundary + "\r\n\r\n")
buffer.WriteString("--" + boundary + "\r\n")
buffer.WriteString("Content-Type: text/plain; charset=utf-8\r\n")
buffer.WriteString("Content-Transfer-Encoding: 8bit\r\n\r\n")
buffer.WriteString(message.Text + "\r\n\r\n")
buffer.WriteString("--" + boundary + "\r\n")
buffer.WriteString("Content-Type: text/html; charset=utf-8\r\n")
buffer.WriteString("Content-Transfer-Encoding: 8bit\r\n\r\n")
buffer.WriteString(message.HTML + "\r\n\r\n")
buffer.WriteString("--" + boundary + "--\r\n")
return buffer.Bytes()
}