restructure project, add claudemd
This commit is contained in:
89
go/snailmail/cloudflare.go
Normal file
89
go/snailmail/cloudflare.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package snailmail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type cloudflareSender struct{}
|
||||
|
||||
type cfEmailRequest struct {
|
||||
To string `json:"to"`
|
||||
From string `json:"from"`
|
||||
Subject string `json:"subject"`
|
||||
HTML string `json:"html,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
}
|
||||
|
||||
type cfEmailResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Errors []struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
} `json:"errors"`
|
||||
}
|
||||
|
||||
func (c *cloudflareSender) Send(message Email, mailtype int) error {
|
||||
if len(message.Recipients) == 0 {
|
||||
return fmt.Errorf("cloudflare email: no recipients")
|
||||
}
|
||||
|
||||
req := cfEmailRequest{
|
||||
To: message.Recipients[0],
|
||||
From: settings.Cloudflare.FromAddress,
|
||||
Subject: message.Subject,
|
||||
}
|
||||
|
||||
if mailtype == TYPE_HTML {
|
||||
req.HTML = message.Body.String()
|
||||
} else {
|
||||
req.Text = message.Body.String()
|
||||
}
|
||||
|
||||
body, err := json.Marshal(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cloudflare email: failed to marshal request: %w", err)
|
||||
}
|
||||
|
||||
url := fmt.Sprintf("https://api.cloudflare.com/client/v4/accounts/%s/email/sending/send", settings.Cloudflare.AccountID)
|
||||
|
||||
httpReq, err := http.NewRequest("POST", url, bytes.NewReader(body))
|
||||
if err != nil {
|
||||
return fmt.Errorf("cloudflare email: failed to create request: %w", err)
|
||||
}
|
||||
|
||||
httpReq.Header.Set("Authorization", "Bearer "+settings.Cloudflare.APIToken)
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(httpReq)
|
||||
if err != nil {
|
||||
log.Println("cloudflare email: request failed:", err)
|
||||
return fmt.Errorf("cloudflare email: request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
respBody, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cloudflare email: failed to read response: %w", err)
|
||||
}
|
||||
|
||||
var cfResp cfEmailResponse
|
||||
if err := json.Unmarshal(respBody, &cfResp); err != nil {
|
||||
return fmt.Errorf("cloudflare email: failed to parse response: %w", err)
|
||||
}
|
||||
|
||||
if !cfResp.Success {
|
||||
errMsg := "unknown error"
|
||||
if len(cfResp.Errors) > 0 {
|
||||
errMsg = cfResp.Errors[0].Message
|
||||
}
|
||||
log.Printf("cloudflare email: send failed: %s (HTTP %d)", errMsg, resp.StatusCode)
|
||||
return fmt.Errorf("cloudflare email: %s", errMsg)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
53
go/snailmail/smtp.go
Normal file
53
go/snailmail/smtp.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package snailmail
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
"mime"
|
||||
"net/mail"
|
||||
"net/smtp"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
type smtpSender struct{}
|
||||
|
||||
func (s *smtpSender) Send(message Email, mailtype int) error {
|
||||
recipientString := strings.Join(message.Recipients, ",")
|
||||
from := mail.Address{Name: settings.SMTP.DisplayFrom, Address: settings.SMTP.Username}
|
||||
|
||||
header := make(map[string]string)
|
||||
header["To"] = recipientString
|
||||
header["From"] = from.String()
|
||||
header["Subject"] = mime.QEncoding.Encode("UTF-8", message.Subject)
|
||||
header["MIME-Version"] = "1.0"
|
||||
header["Content-Transfer-Encoding"] = "base64"
|
||||
header["Date"] = time.Now().Format(time.RFC1123)
|
||||
|
||||
if mailtype == TYPE_HTML {
|
||||
header["Content-Type"] = "text/html; charset=\"utf-8\""
|
||||
} else {
|
||||
header["Content-Type"] = "text/plain; charset=\"utf-8\""
|
||||
}
|
||||
|
||||
email := ""
|
||||
for k, v := range header {
|
||||
email += fmt.Sprintf("%s: %s\r\n", k, v)
|
||||
}
|
||||
email += "\r\n" + base64.StdEncoding.EncodeToString(message.Body.Bytes())
|
||||
|
||||
var auth smtp.Auth = nil
|
||||
|
||||
if settings.SMTP.RequireAuth {
|
||||
auth = smtp.PlainAuth("", settings.SMTP.Username, settings.SMTP.Password, settings.SMTP.Server)
|
||||
}
|
||||
|
||||
err := smtp.SendMail(settings.SMTP.Server+":"+settings.SMTP.Port, auth, settings.SMTP.Username, message.Recipients, []byte(email))
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
81
go/snailmail/snailmail.go
Normal file
81
go/snailmail/snailmail.go
Normal file
@@ -0,0 +1,81 @@
|
||||
// package snailmail sends email through a pluggable provider (SMTP or Cloudflare).
|
||||
// The active provider and its credentials are injected via Configure(Settings),
|
||||
// so the framework never reads application config. Branded message composition
|
||||
// (templates, logos, copy) stays app-side: apps build an Email and call SendMail.
|
||||
package snailmail
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
TYPE_TEXT = iota
|
||||
TYPE_HTML
|
||||
)
|
||||
|
||||
// Email is a composed message ready to send. Body holds the already-rendered
|
||||
// text or HTML.
|
||||
type Email struct {
|
||||
Recipients []string
|
||||
Subject string
|
||||
Body *bytes.Buffer
|
||||
}
|
||||
|
||||
// SMTPSettings holds credentials for the SMTP provider.
|
||||
type SMTPSettings struct {
|
||||
Server string
|
||||
Port string
|
||||
Username string
|
||||
Password string
|
||||
DisplayFrom string
|
||||
RequireAuth bool
|
||||
}
|
||||
|
||||
// CloudflareSettings holds credentials for the Cloudflare email provider.
|
||||
type CloudflareSettings struct {
|
||||
AccountID string
|
||||
APIToken string
|
||||
FromAddress string
|
||||
}
|
||||
|
||||
// Settings selects and configures the active email provider. Provider is "smtp"
|
||||
// (default) or "cloudflare".
|
||||
type Settings struct {
|
||||
Provider string
|
||||
SMTP SMTPSettings
|
||||
Cloudflare CloudflareSettings
|
||||
}
|
||||
|
||||
type sender interface {
|
||||
Send(message Email, mailtype int) error
|
||||
}
|
||||
|
||||
var (
|
||||
settings Settings
|
||||
activeSender sender
|
||||
)
|
||||
|
||||
// Configure stores the provider credentials and selects the active provider.
|
||||
// Apps call this once at startup with values from their own config.
|
||||
func Configure(s Settings) {
|
||||
settings = s
|
||||
switch strings.ToLower(strings.TrimSpace(s.Provider)) {
|
||||
case "cloudflare":
|
||||
activeSender = &cloudflareSender{}
|
||||
log.Println("mailer: using Cloudflare email provider")
|
||||
default:
|
||||
activeSender = &smtpSender{}
|
||||
log.Println("mailer: using SMTP email provider")
|
||||
}
|
||||
}
|
||||
|
||||
// SendMail sends message using the configured provider (defaulting to SMTP if
|
||||
// Configure was never called).
|
||||
func SendMail(message Email, mailtype int) error {
|
||||
if activeSender == nil {
|
||||
activeSender = &smtpSender{}
|
||||
}
|
||||
return activeSender.Send(message, mailtype)
|
||||
}
|
||||
Reference in New Issue
Block a user