summaryrefslogtreecommitdiffci
path: root/internal/mailer
diff refs
from: back
to: back
| flip
diff options
context:
space:
mode:
Diffstat (limited to 'internal/mailer')
-rw-r--r--internal/mailer/mailer.go289
1 files changed, 15 insertions, 274 deletions
diff --git a/internal/mailer/mailer.go b/internal/mailer/mailer.go
index 33db4f7..450b818 100644
--- a/internal/mailer/mailer.go
+++ b/internal/mailer/mailer.go
@@ -6,17 +6,14 @@ import (
"encoding/base64"
"encoding/binary"
"fmt"
- "html/template"
"log/slog"
- "slices"
"strconv"
"strings"
"sync"
"time"
- "github.com/SayaAndy/saya-today-web/internal/blog"
"github.com/SayaAndy/saya-today-web/internal/templatemanager"
- "github.com/SayaAndy/saya-today-web/l10n"
+ "github.com/SayaAndy/saya-today-web/locale"
"github.com/dgraph-io/ristretto/v2"
"github.com/gofiber/fiber/v2"
"github.com/wneessen/go-mail"
@@ -25,7 +22,6 @@ import (
type Mailer struct {
verificationCodes *ristretto.Cache[uint64, string]
- unsubscribeCodes *ristretto.Cache[uint64, []byte]
db *sql.DB
tm *templatemanager.TemplateManager
mailClient *mail.Client
@@ -43,32 +39,16 @@ type Mailer struct {
hashMap map[string][]byte
hashMapMutex sync.RWMutex
-}
-
-type SubscriptionType int
-const (
- All SubscriptionType = iota
- None
- Specific
-)
+ l map[string]*locale.LocaleConfig
+}
-func NewMailer(db *sql.DB, clientHost string, mailHost string, publicName string, mailAddress string, username string, password string, salt []byte) (*Mailer, error) {
+func NewMailer(db *sql.DB, clientHost string, mailHost string, publicName string, mailAddress string, username string, password string, salt []byte, localization map[string]*locale.LocaleConfig) (*Mailer, error) {
verificationCodes, err := ristretto.NewCache(&ristretto.Config[uint64, string]{
NumCounters: 10000,
MaxCost: 1 << 20, // 1 MB
BufferItems: 64,
- TtlTickerDurationInSec: 3600, // 1 hour
- })
- if err != nil {
- return nil, fmt.Errorf("fail to initialize cache for verification codes: %w", err)
- }
-
- unsubscribeCodes, err := ristretto.NewCache(&ristretto.Config[uint64, []byte]{
- NumCounters: 10000,
- MaxCost: 1 << 20, // 1 MB
- BufferItems: 64,
- TtlTickerDurationInSec: 86400, // 1 day
+ TtlTickerDurationInSec: 3600,
})
if err != nil {
return nil, fmt.Errorf("fail to initialize cache for verification codes: %w", err)
@@ -95,7 +75,6 @@ func NewMailer(db *sql.DB, clientHost string, mailHost string, publicName string
return &Mailer{
verificationCodes: verificationCodes,
- unsubscribeCodes: unsubscribeCodes,
db: db,
clientHost: clientHost,
tm: tm,
@@ -109,7 +88,7 @@ func NewMailer(db *sql.DB, clientHost string, mailHost string, publicName string
End time.Time
CodeExpiry time.Time
}, 0),
- }, nil
+ l: localization}, nil
}
func (m *Mailer) GetHash(id string) []byte {
@@ -150,45 +129,31 @@ func (m *Mailer) MailIsTaken(email string) (bool, error) {
return false, fmt.Errorf("failed to initialize transaction with db: %s", err)
}
- slog.Debug("began db transaction", slog.String("method", "MailIsTaken"))
- defer func(tx *sql.Tx) {
- if err = tx.Commit(); err != nil {
- tx.Rollback()
- }
- slog.Debug("ended db transaction", slog.String("method", "MailIsTaken"))
- }(tx)
-
var rows *sql.Rows
if rows, err = tx.Query(`SELECT email FROM user_email_table WHERE email=? LIMIT 1;`, email); err != nil {
tx.Rollback()
-
return false, fmt.Errorf("failed to query user-email settings in db: %s", err)
}
+ defer tx.Commit()
defer rows.Close()
- isTaken := rows.Next()
- return isTaken, nil
+ return rows.Next(), nil
}
-func (m *Mailer) GetInfo(userIdHash []byte) (email string, lang string, err error) {
+func (m *Mailer) GetInfo(userId string) (email string, lang string, err error) {
+ hash := m.GetHash(userId)
+
tx, err := m.db.Begin()
if err != nil {
return "", "", fmt.Errorf("failed to initialize transaction with db: %s", err)
}
- slog.Debug("began db transaction", slog.String("method", "GetInfo"))
- defer func(tx *sql.Tx) {
- if err = tx.Commit(); err != nil {
- tx.Rollback()
- }
- slog.Debug("ended db transaction", slog.String("method", "GetInfo"))
- }(tx)
-
var rows *sql.Rows
- if rows, err = tx.Query(`SELECT email, lang FROM user_email_table WHERE user_id=? LIMIT 1;`, userIdHash); err != nil {
+ if rows, err = tx.Query(`SELECT email, lang FROM user_email_table WHERE user_id=? LIMIT 1;`, hash); err != nil {
tx.Rollback()
return "", "", fmt.Errorf("failed to query user-email settings in db: %s", err)
}
+ defer tx.Commit()
defer rows.Close()
if !rows.Next() {
@@ -201,24 +166,6 @@ func (m *Mailer) GetInfo(userIdHash []byte) (email string, lang string, err erro
return
}
-func (m *Mailer) Unsubscribe(unsubscribeCodeString string) (clientError error, serverError error) {
- unsubscribeCode, err := strconv.ParseUint(unsubscribeCodeString, 16, 64)
- if err != nil {
- return fmt.Errorf("invalid unsubscribe code: %s", err), nil
- }
-
- userId, _ := m.unsubscribeCodes.Get(unsubscribeCode)
- if len(userId) == 0 {
- return fmt.Errorf("invalid unsubscribe code: have no information about it"), nil
- }
-
- if err = m.Subscribe(userId, None); err != nil {
- return nil, fmt.Errorf("failed to unsubscribe: %s", err)
- }
-
- return nil, nil
-}
-
func (m *Mailer) SendVerificationCode(userId string, address string, lang string) error {
message := mail.NewMsg()
@@ -259,9 +206,10 @@ func (m *Mailer) SendVerificationCode(userId string, address string, lang string
verificationInfo := fmt.Sprintf("%s.%s", base64.RawStdEncoding.EncodeToString([]byte(userId)), base64.RawStdEncoding.EncodeToString([]byte(address)))
m.verificationCodes.Set(verificationCode, verificationInfo, int64(len(verificationInfo)+8))
- message.Subject(l10n.T.GetPath(lang, "Mail", "VerifyEmail", "Subject").(string))
+ message.Subject(m.l[lang].Mail.VerifyEmail.Subject)
msg, err := m.tm.Render("verify-email", fiber.Map{
+ "L": m.l[lang],
"Lang": lang,
"VerificationCode": fmt.Sprintf("%X", verificationCode),
"ClientHost": m.clientHost,
@@ -314,227 +262,20 @@ func (m *Mailer) Verify(verificationCodeEncoded string, lang string) error {
return fmt.Errorf("failed to initialize transaction with db: %s", err)
}
- slog.Debug("began db transaction", slog.String("method", "Verify"))
if _, err = tx.Exec(`INSERT INTO user_email_table(user_id, email, lang) VALUES(?, ?, ?)
ON CONFLICT(user_id) DO UPDATE SET
email=excluded.email,
lang=excluded.lang;`, m.GetHash(string(userId)), address, lang); err != nil {
tx.Rollback()
- slog.Debug("ended db transaction", slog.String("method", "Verify"))
return fmt.Errorf("failed to configure user-email settings in db: %s", err)
}
if err = tx.Commit(); err != nil {
tx.Rollback()
- slog.Debug("ended db transaction", slog.String("method", "Verify"))
return fmt.Errorf("failed to commit transaction to db: %s", err)
}
- slog.Debug("ended db transaction", slog.String("method", "Verify"))
m.verificationCodes.Del(verificationCode)
delete(m.lostMailMap, verificationSegments[0])
return nil
}
-
-func (m *Mailer) GetSubscriptions(userId string) (subscriptionType SubscriptionType, tags []string, err error) {
- tx, err := m.db.Begin()
- if err != nil {
- return None, nil, fmt.Errorf("failed to initialize transaction with db: %s", err)
- }
-
- slog.Debug("began db transaction", slog.String("method", "GetSubscriptions"))
- defer func(tx *sql.Tx) {
- if err = tx.Commit(); err != nil {
- tx.Rollback()
- }
- slog.Debug("ended db transaction", slog.String("method", "GetSubscriptions"))
- }(tx)
-
- hash := m.GetHash(userId)
-
- var rows *sql.Rows
- if rows, err = tx.Query(`SELECT tags FROM subscription_user_to_tags_table WHERE user_id=? LIMIT 1;`, hash); err != nil {
- tx.Rollback()
- return None, nil, fmt.Errorf("failed to query user-to-tags table in db for the user: %s", err)
- }
- defer rows.Close()
-
- if !rows.Next() {
- return None, nil, nil
- }
-
- tagsString := ""
- if err = rows.Scan(&tagsString); err != nil {
- return None, nil, fmt.Errorf("failed to scan the result from user-to-tags query: %s", err)
- }
-
- switch tagsString {
- case "":
- return None, nil, nil
- case "_all":
- return All, nil, nil
- default:
- return Specific, strings.Split(tagsString, ","), nil
- }
-}
-
-func (m *Mailer) Subscribe(userIdHash []byte, subscriptionType SubscriptionType, tags ...string) error {
- tx, err := m.db.Begin()
- if err != nil {
- return fmt.Errorf("failed to initialize transaction with db: %s", err)
- }
-
- slog.Debug("began db transaction", slog.String("method", "Subscribe"))
-
- slices.Sort(tags)
- tagsOutput := ""
- switch subscriptionType {
- case All:
- tagsOutput = "_all"
- case None:
- tagsOutput = ""
- case Specific:
- tagsOutput = strings.Join(tags, ",")
- }
-
- if _, err = tx.Exec(`INSERT INTO subscription_user_to_tags_table(user_id, tags) VALUES(?, ?)
- ON CONFLICT(user_id) DO UPDATE SET
- tags=excluded.tags;`, userIdHash, tagsOutput); err != nil {
- tx.Rollback()
- slog.Debug("ended db transaction", slog.String("method", "Subscribe"))
- return fmt.Errorf("failed to configure user-to-tags table in db for the user: %s", err)
- }
-
- if err = tx.Commit(); err != nil {
- tx.Rollback()
- slog.Debug("ended db transaction", slog.String("method", "Subscribe"))
- return fmt.Errorf("failed to commit transaction to db: %s", err)
- }
- slog.Debug("ended db transaction", slog.String("method", "Subscribe"))
-
- return nil
-}
-
-func (m *Mailer) NewPost(post *blog.Page) error {
- tx, err := m.db.Begin()
- if err != nil {
- return fmt.Errorf("failed to initialize transaction with db: %s", err)
- }
-
- slog.Debug("began db transaction", slog.String("method", "NewPost"))
- var rows *sql.Rows
- if rows, err = tx.Query(`SELECT user_id, tags FROM subscription_user_to_tags_table;`); err != nil {
- tx.Rollback()
- slog.Debug("ended db transaction", slog.String("method", "NewPost"))
- return fmt.Errorf("failed to query user-to-tags table in db: %s", err)
- }
-
- var userId []byte
- usersToSend := make([]struct {
- userId []byte
- email string
- }, 0)
- var tagsString string
- i := -1
-
-rowLoop:
- for rows.Next() {
- i++
- if err = rows.Scan(&userId, &tagsString); err != nil {
- slog.Warn("failed to scan a row in user-to-tags table", slog.String("error", err.Error()), slog.Int("index", i), slog.String("user_id", base64.RawStdEncoding.EncodeToString(userId)))
- continue
- }
-
- if tagsString == "" {
- continue
- }
-
- if tagsString == "_all" {
- usersToSend = append(usersToSend, struct {
- userId []byte
- email string
- }{userId, ""})
- continue
- }
-
- pageTags := strings.Split(tagsString, ",")
- for _, tag := range pageTags {
- if _, found := slices.BinarySearch(post.Metadata.Tags, tag); found {
- usersToSend = append(usersToSend, struct {
- userId []byte
- email string
- }{userId, ""})
- continue rowLoop
- }
- }
- }
-
- tx.Commit()
- rows.Close()
- slog.Debug("ended db transaction", slog.String("method", "NewPost"))
-
- for i := range usersToSend {
- email, lang, err := m.GetInfo(usersToSend[i].userId)
- if err != nil {
- slog.Warn("failed to get info about the user", slog.String("error", err.Error()), slog.Int("index", i), slog.String("user_id", base64.RawStdEncoding.EncodeToString(userId)))
- continue
- }
-
- if lang != post.Lang {
- continue
- }
-
- usersToSend[i].email = email
- }
-
- messages := make([]*mail.Msg, 0, len(usersToSend))
- for _, user := range usersToSend {
- if user.email == "" {
- continue
- }
- unsubscribeCodeBytes := make([]byte, 8)
- rand.Read(unsubscribeCodeBytes)
- unsubscribeCode := binary.LittleEndian.Uint64(unsubscribeCodeBytes)
-
- unsubscribeFooter := strings.Replace(l10n.T.GetPath(post.Lang, "Mail", "UnsubscribeFooter").(string), "{}", fmt.Sprintf(`<a style="color: #273de1 !important;" href="https://%s/%s/user/unsubscribe?code=%X">`, m.clientHost, post.Lang, unsubscribeCode), 1)
- unsubscribeFooter = strings.Replace(unsubscribeFooter, "{/}", "</a>", 1)
-
- msgBody, err := m.tm.Render("new-post", fiber.Map{
- "Lang": post.Lang,
- "Post": post,
- "ClientHost": m.clientHost,
- "UnsubscribeFooter": template.HTML(unsubscribeFooter),
- })
-
- if err != nil {
- return fmt.Errorf("failed to render message body: %w", err)
- }
-
- message := mail.NewMsg()
-
- if err := message.EnvelopeFrom(m.mailAddress); err != nil {
- return fmt.Errorf("failed to set ENVELOPE FROM address: %w", err)
- }
- if err := message.FromFormat(m.publicName, m.mailAddress); err != nil {
- return fmt.Errorf("failed to set formatted FROM address: %w", err)
- }
- if err := message.To(user.email); err != nil {
- return fmt.Errorf("failed to set TO address: %w", err)
- }
-
- message.SetMessageID()
- message.SetDate()
- message.SetBulk()
- message.Subject(l10n.T.GetPath(post.Lang, "Mail", "NewPost", "Subject").(string))
- message.SetBodyString(mail.TypeTextHTML, string(msgBody))
-
- m.unsubscribeCodes.Set(unsubscribeCode, user.userId, 40)
-
- messages = append(messages, message)
- }
-
- if err := m.mailClient.DialAndSend(messages...); err != nil {
- return fmt.Errorf("failed to send new post notifications: %w", err)
- }
- return nil
-}