Files
gochat/backend/internal/service/contact_export_mailer.go
T
rogee aeddedf2a3 Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories:
- backend/: Go module root (cmd, internal, pkg, configs, migrations,
  docs/swagger, scripts, tests, go.mod, Makefile, .air.toml)
- deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd
- docs/: project documentation + reports/ (moved from repo root)
- AGENTS.md: new AI coding-agent guide at repo root

Update all references to the new layout:
- Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root)
- docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile,
  env_file ../../.env, volume mounts ../../backend:/app
- deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile
- CI: working-directory: backend for go commands, file deploy/docker/Dockerfile,
  coverage path backend/coverage.out, health_check backend/scripts/
- backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../
- README: architecture tree, quickstart, config paths updated

Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh,
gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to
preserve history. Build, vet, SQLite tests, and docker compose config verified.
2026-07-07 14:44:12 +08:00

113 lines
3.2 KiB
Go

package service
import (
"context"
"fmt"
"net/mail"
"net/smtp"
"os"
"strconv"
"strings"
"github.com/gochat/gochat/internal/model"
)
const contactExportCompleteSubject = "Your contact's export file is available to download."
// ContactExportMailer delivers the administrator notification emitted by
// Chatwoot's Account::ContactsExportJob after the CSV artifact is attached.
type ContactExportMailer interface {
SendContactExportComplete(ctx context.Context, account *model.Account, user *model.User, export *model.ContactExport) error
}
type SMTPContactExportMailer struct {
Address string
Port int
Username string
Password string
From string
FrontendURL string
}
func NewEnvContactExportMailer() *SMTPContactExportMailer {
return &SMTPContactExportMailer{
Address: strings.TrimSpace(os.Getenv("SMTP_ADDRESS")),
Port: envInt("SMTP_PORT", 587),
Username: firstEnv("SMTP_USERNAME", "SMTP_LOGIN"),
Password: os.Getenv("SMTP_PASSWORD"),
From: firstEnv("MAILER_SENDER_EMAIL", "SMTP_FROM"),
FrontendURL: strings.TrimRight(os.Getenv("FRONTEND_URL"), "/"),
}
}
func (m *SMTPContactExportMailer) SendContactExportComplete(ctx context.Context, account *model.Account, user *model.User, export *model.ContactExport) error {
_ = ctx
if m == nil || strings.TrimSpace(m.Address) == "" || user == nil || strings.TrimSpace(user.Email) == "" || export == nil {
return nil
}
fromHeader := strings.TrimSpace(m.From)
if fromHeader == "" {
fromHeader = "Chatwoot <accounts@chatwoot.com>"
}
fromAddress := fromHeader
if parsed, err := mail.ParseAddress(fromHeader); err == nil {
fromAddress = parsed.Address
}
to := strings.TrimSpace(user.Email)
body := contactExportEmailBody(account, export, m.FrontendURL)
message := smtpMessage(fromHeader, to, contactExportCompleteSubject, body)
addr := fmt.Sprintf("%s:%d", strings.TrimSpace(m.Address), m.Port)
var auth smtp.Auth
if strings.TrimSpace(m.Username) != "" {
auth = smtp.PlainAuth("", strings.TrimSpace(m.Username), m.Password, strings.TrimSpace(m.Address))
}
return smtp.SendMail(addr, auth, fromAddress, []string{to}, []byte(message))
}
func contactExportEmailBody(account *model.Account, export *model.ContactExport, frontendURL string) string {
accountName := "your account"
if account != nil && strings.TrimSpace(account.Name) != "" {
accountName = account.Name
}
fileURL := export.FileURL
if frontendURL != "" && strings.HasPrefix(fileURL, "/") {
fileURL = frontendURL + fileURL
}
return fmt.Sprintf("The contact export for %s is ready.\n\nDownload: %s\n", accountName, fileURL)
}
func smtpMessage(from, to, subject, body string) string {
return strings.Join([]string{
"From: " + from,
"To: " + to,
"Subject: " + subject,
"MIME-Version: 1.0",
"Content-Type: text/plain; charset=UTF-8",
"",
body,
}, "\r\n")
}
func firstEnv(keys ...string) string {
for _, key := range keys {
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
return value
}
}
return ""
}
func envInt(key string, fallback int) int {
value := strings.TrimSpace(os.Getenv(key))
if value == "" {
return fallback
}
parsed, err := strconv.Atoi(value)
if err != nil || parsed <= 0 {
return fallback
}
return parsed
}