This commit is contained in:
2026-02-11 17:46:22 +03:00
commit eacfacb13b
266 changed files with 51337 additions and 0 deletions

View File

@@ -0,0 +1,270 @@
package mailer
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"os"
"strconv"
"strings"
"time"
"golang.org/x/oauth2/clientcredentials"
)
type GraphMailer struct {
httpClient *http.Client
from string // noreply@baggi.com.tr
replyTo string // opsiyonel
}
func NewGraphMailer() (*GraphMailer, error) {
tenantID := strings.TrimSpace(os.Getenv("AZURE_TENANT_ID"))
clientID := strings.TrimSpace(os.Getenv("AZURE_CLIENT_ID"))
clientSecret := strings.TrimSpace(os.Getenv("AZURE_CLIENT_SECRET"))
from := strings.TrimSpace(os.Getenv("MAIL_FROM"))
replyTo := strings.TrimSpace(os.Getenv("MAIL_REPLY_TO")) // opsiyonel
if tenantID == "" || clientID == "" || clientSecret == "" || from == "" {
return nil, fmt.Errorf("azure graph mailer env missing (AZURE_TENANT_ID/AZURE_CLIENT_ID/AZURE_CLIENT_SECRET/MAIL_FROM)")
}
conf := clientcredentials.Config{
ClientID: clientID,
ClientSecret: clientSecret,
TokenURL: fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", tenantID),
Scopes: []string{"https://graph.microsoft.com/.default"},
}
httpClient := conf.Client(context.Background())
httpClient.Timeout = 25 * time.Second
log.Printf("✉️ Graph Mailer hazır (App-only token) | from=%s", from)
return &GraphMailer{
httpClient: httpClient,
from: from,
replyTo: replyTo,
}, nil
}
func (g *GraphMailer) Send(ctx context.Context, msg Message) error {
start := time.Now()
// ---------- validate ----------
cleanTo := make([]string, 0, len(msg.To))
for _, t := range msg.To {
t = strings.TrimSpace(t)
if t != "" {
cleanTo = append(cleanTo, t)
}
}
if len(cleanTo) == 0 {
return fmt.Errorf("recipient missing")
}
subject := strings.TrimSpace(msg.Subject)
if subject == "" {
return fmt.Errorf("subject missing")
}
// internal-safe adjustments
isInternal := allRecipientsInDomain(cleanTo, "baggi.com.tr")
if isInternal && !strings.HasPrefix(subject, "[BSSApp]") {
subject = "[BSSApp] " + subject
}
html := strings.TrimSpace(msg.BodyHTML)
text := strings.TrimSpace(msg.Body)
// Eğer sadece HTML geldiyse text üret (spam skorunu düşürür)
if text == "" && html != "" {
text = stripHTMLVerySimple(html)
}
// Eğer sadece text geldiyse basit HTML üret
if html == "" && text != "" {
html = "<pre style=\"font-family:Segoe UI, Arial, sans-serif; white-space:pre-wrap;\">" +
htmlEscape(text) + "</pre>"
}
if html == "" {
return fmt.Errorf("body missing (Body or BodyHTML)")
}
log.Printf("✉️ [MAIL] SEND START | from=%s | to=%v | internal=%v | subject=%s", g.from, cleanTo, isInternal, subject)
// ---------- build recipients ----------
type recipient struct {
EmailAddress struct {
Address string `json:"address"`
} `json:"emailAddress"`
}
toRecipients := make([]recipient, 0, len(cleanTo))
for _, m := range cleanTo {
r := recipient{}
r.EmailAddress.Address = m
toRecipients = append(toRecipients, r)
}
// ---------- headers to reduce auto-phish/spam signals ----------
headers := []map[string]string{
{
"name": "X-Mailer",
"value": "BSSApp Graph Mailer",
},
{
"name": "X-BSSApp-Internal",
"value": strconv.FormatBool(isInternal),
},
}
// replyTo (opsiyonel)
var replyToRecipients []recipient
if strings.TrimSpace(g.replyTo) != "" {
rt := recipient{}
rt.EmailAddress.Address = strings.TrimSpace(g.replyTo)
replyToRecipients = []recipient{rt}
}
// ---------- Graph payload ----------
message := map[string]any{
"subject": subject,
"body": map[string]string{
"contentType": "HTML",
"content": buildHTML(html, text, isInternal),
},
"toRecipients": toRecipients,
"internetMessageHeaders": headers,
"importance": "normal",
}
// replyTo SADECE doluysa ekle
if len(replyToRecipients) > 0 {
message["replyTo"] = replyToRecipients
}
payload := map[string]any{
"message": message,
"saveToSentItems": true,
}
b, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("json marshal: %w", err)
}
url := fmt.Sprintf(
"https://graph.microsoft.com/v1.0/users/%s/sendMail",
g.from,
)
req, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
url,
bytes.NewBuffer(b),
)
if err != nil {
return fmt.Errorf("new request: %w", err)
}
req.Header.Set("Content-Type", "application/json")
res, err := g.httpClient.Do(req)
if err != nil {
return fmt.Errorf("graph request: %w", err)
}
defer res.Body.Close()
if res.StatusCode >= 300 {
bodyBytes, _ := io.ReadAll(res.Body)
log.Printf(
"❌ [MAIL] SEND FAILED | status=%s | body=%s",
res.Status,
string(bodyBytes),
)
return fmt.Errorf("graph send mail failed: %s", res.Status)
}
log.Printf(
"✅ [MAIL] SEND OK | to=%v | duration=%s",
cleanTo,
time.Since(start),
)
return nil
}
// ---------- helpers ----------
func allRecipientsInDomain(to []string, domain string) bool {
domain = strings.ToLower(strings.TrimSpace(domain))
for _, addr := range to {
addr = strings.ToLower(strings.TrimSpace(addr))
if !strings.HasSuffix(addr, "@"+domain) {
return false
}
}
return true
}
func buildHTML(htmlBody, textBody string, internal bool) string {
// Internal ise daha sade, daha az “marketing-like”
if internal {
return `
<div style="font-family:Segoe UI, Arial, sans-serif; font-size:14px; color:#1f1f1f;">
` + htmlBody + `
<hr style="border:none;border-top:1px solid #e5e5e5;margin:16px 0;" />
<div style="font-size:12px;color:#666;">
Bu e-posta BSSApp sistemi tarafından otomatik oluşturulmuştur.
</div>
</div>`
}
// External
return `
<div style="font-family:Segoe UI, Arial, sans-serif; font-size:14px; color:#1f1f1f;">
` + htmlBody + `
</div>`
}
// Çok basit “html -> text” (tam değil ama yeterli)
func stripHTMLVerySimple(s string) string {
s = strings.ReplaceAll(s, "<br>", "\n")
s = strings.ReplaceAll(s, "<br/>", "\n")
s = strings.ReplaceAll(s, "<br />", "\n")
s = strings.ReplaceAll(s, "</p>", "\n\n")
// kaba temizlik
for {
i := strings.Index(s, "<")
j := strings.Index(s, ">")
if i >= 0 && j > i {
s = s[:i] + s[j+1:]
continue
}
break
}
return strings.TrimSpace(s)
}
func htmlEscape(s string) string {
replacer := strings.NewReplacer(
"&", "&amp;",
"<", "&lt;",
">", "&gt;",
"\"", "&quot;",
"'", "&#39;",
)
return replacer.Replace(s)
}
func (g *GraphMailer) SendMail(to string, subject string, html string) error {
msg := Message{
To: []string{to},
Subject: subject,
BodyHTML: html,
}
// context burada internal olarak veriliyor
return g.Send(context.Background(), msg)
}