fix(admin): SMTP over implicit TLS, and rollbacks that survive
Server Deploy / deploy (push) Successful in 2m13s
Server Deploy / deploy (push) Successful in 2m13s
Two bugs, one symptom: signup created an account and a customer_user but no verification email ever arrived. net/smtp.SendMail only speaks STARTTLS. Against a port-465 server, which expects a TLS handshake immediately, it never delivers. The transport now wraps the connection before speaking SMTP on 465, exactly as sitesvc/internal/mail already did — the two are duplicated, so change both or consolidate into shared/. Also adds Date and Message-ID, whose absence gets a message scored as spam, and a 15s deadline on the conversation. The rollbacks ran on the HTTP request's context. A stalled mail server holds the request until the browser gives up, which cancels that context and turns both rollbacks into silent no-ops — stranding the exact rows they exist to remove. They now run detached with their own timeout, and log when they fail. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -61,7 +62,18 @@ func CreateCustomerUser(ctx context.Context, accountID, email, password string)
|
||||
// worse than no row: it can never be signed in to, and it holds the
|
||||
// unique index on email, so the customer cannot sign up again with the
|
||||
// address they just used.
|
||||
_, _ = db.Admin("customer_users").DeleteOne(ctx, bson.M{"user_id": u.UserID})
|
||||
//
|
||||
// Deliberately NOT on ctx. ctx is the HTTP request's, and the most likely
|
||||
// reason we are here is that the mail server stalled until the browser
|
||||
// gave up — which cancels ctx and makes this delete a silent no-op,
|
||||
// stranding exactly the row it exists to remove. That happened in
|
||||
// production against a port-465 server.
|
||||
rbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second)
|
||||
defer cancel()
|
||||
if _, dErr := db.Admin("customer_users").DeleteOne(rbCtx, bson.M{"user_id": u.UserID}); dErr != nil {
|
||||
log.Printf("signup: FAILED to roll back customer_user %s (%s) after mail error: %v",
|
||||
u.UserID, u.Email, dErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -123,8 +135,16 @@ func HandleSignup(c *gin.Context) {
|
||||
}
|
||||
|
||||
if err := CreateCustomerUser(ctx, acct.AccountID, email, body.Password); err != nil {
|
||||
// Roll the account back rather than strand one with no owner.
|
||||
_, _ = db.Admin("accounts").DeleteOne(ctx, bson.M{"account_id": acct.AccountID})
|
||||
// Roll the account back rather than strand one with no owner. Detached
|
||||
// from ctx for the same reason as the user rollback above: a stalled mail
|
||||
// server cancels the request, and a rollback that needs the request to
|
||||
// still be alive is a rollback that fails exactly when it is needed.
|
||||
log.Printf("signup: %s failed, rolling back account %s: %v", email, acct.AccountID, err)
|
||||
rbCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 10*time.Second)
|
||||
defer cancel()
|
||||
if _, dErr := db.Admin("accounts").DeleteOne(rbCtx, bson.M{"account_id": acct.AccountID}); dErr != nil {
|
||||
log.Printf("signup: FAILED to roll back account %s: %v", acct.AccountID, dErr)
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "could not send the verification email"})
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user