feat(account): brand and harden the email pipeline
Swap the net/smtp mailer for go-mail behind the existing Mailer seam: the Message struct now carries a text + HTML body, TLS mode is chosen from the port (implicit TLS on 465, else mandatory STARTTLS), a dial timeout bounds the synchronous send, and no client certificate is needed. Add a branded, image-free, mobile-friendly ru/en HTML template (with a plain-text alternative) rendering a large readable code and an ignore-notice footer with a landing link. Add BACKEND_PUBLIC_BASE_URL config (the canonical origin for the email footer link, never the request Host — anti-injection), required when a relay is configured. Fix the email-login address squat: ProvisionEmail creates the account flagged is_guest until the code is confirmed, so an abandoned login is reaped like any guest and its address freed; confirming (login or link) clears the flag. Seed the new account's language from the client, plumbed through the email-login request. The confirm deeplink, its transport surface and the send rate-limit land in follow-up work.
This commit is contained in:
@@ -121,13 +121,30 @@ func (s *Store) ProvisionByIdentity(ctx context.Context, kind, externalID string
|
||||
}
|
||||
|
||||
// ProvisionEmail returns the account owning the email identity externalID, creating
|
||||
// it (unconfirmed) on first contact with browserTZ — the client's detected "±HH:MM"
|
||||
// UTC offset — seeded into its time zone. Like ProvisionByIdentity it is race-safe
|
||||
// and leaves an existing account untouched, so a returning user's saved zone is never
|
||||
// overwritten. The email account is created here (the code-request step), not at the
|
||||
// later login, so this is where its zone is seeded.
|
||||
func (s *Store) ProvisionEmail(ctx context.Context, externalID, browserTZ string) (Account, error) {
|
||||
return s.provision(ctx, KindEmail, externalID, provisionSeed{timeZone: seedZone(browserTZ)})
|
||||
// it on first contact with browserTZ — the client's detected "±HH:MM" UTC offset —
|
||||
// seeded into its time zone and language seeded from the client's UI language. Like
|
||||
// ProvisionByIdentity it is race-safe and leaves an existing account untouched, so a
|
||||
// returning user's saved zone and language are never overwritten. The email account is
|
||||
// created here (the code-request step), not at the later login, so this is where its
|
||||
// zone and language are seeded. It is created flagged is_guest with an unconfirmed
|
||||
// email identity: an abandoned, never-confirmed login is then reaped like any guest,
|
||||
// freeing the reserved address, and confirming the code clears the guest flag.
|
||||
func (s *Store) ProvisionEmail(ctx context.Context, externalID, browserTZ, language string) (Account, error) {
|
||||
return s.provision(ctx, KindEmail, externalID, provisionSeed{
|
||||
timeZone: seedZone(browserTZ),
|
||||
preferredLanguage: supportedLanguage(language),
|
||||
isGuest: true,
|
||||
})
|
||||
}
|
||||
|
||||
// supportedLanguage returns code normalised to a supported UI language ("en" or
|
||||
// "ru"), or "" when it maps to neither, so a new account keeps the 'en' default. It
|
||||
// accepts region-tagged codes ("ru-RU").
|
||||
func supportedLanguage(code string) string {
|
||||
if lang, _, _ := strings.Cut(strings.ToLower(strings.TrimSpace(code)), "-"); lang == "en" || lang == "ru" {
|
||||
return lang
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ProvisionRobot provisions (or finds) the durable account backing a robot pool
|
||||
@@ -239,6 +256,11 @@ type provisionSeed struct {
|
||||
preferredLanguage string
|
||||
displayName string
|
||||
timeZone string
|
||||
// isGuest creates the account flagged is_guest. It is set for an email-login
|
||||
// account, which stays a guest until the address is confirmed (so an abandoned,
|
||||
// never-confirmed login is reaped and its address freed); confirming clears the
|
||||
// flag. Platform identities (telegram/vk) are durable from creation.
|
||||
isGuest bool
|
||||
}
|
||||
|
||||
// seedZone returns browserTZ when it is a well-formed zone to persist at account
|
||||
@@ -447,8 +469,8 @@ func (s *Store) create(ctx context.Context, kind, externalID string, seed provis
|
||||
tz = "UTC"
|
||||
}
|
||||
insertAccount := table.Accounts.
|
||||
INSERT(table.Accounts.AccountID, table.Accounts.DisplayName, table.Accounts.PreferredLanguage, table.Accounts.TimeZone).
|
||||
VALUES(accountID, seed.displayName, lang, tz).
|
||||
INSERT(table.Accounts.AccountID, table.Accounts.DisplayName, table.Accounts.PreferredLanguage, table.Accounts.TimeZone, table.Accounts.IsGuest).
|
||||
VALUES(accountID, seed.displayName, lang, tz, seed.isGuest).
|
||||
RETURNING(table.Accounts.AllColumns)
|
||||
|
||||
var row model.Accounts
|
||||
|
||||
Reference in New Issue
Block a user