{"record":{"id":"0bf5d7033d665f07","repo":"knadh/listmonk","slug":"token-was-not-found-or-has-expired","errorCode":null,"errorMessage":"token was not found or has expired","messagePattern":"token was not found or has expired","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/tmptokens/tmptokens.go","lineNumber":27,"sourceCode":"\t\"time\"\n)\n\nconst (\n\t// maxTries is the maximum number of verification attempts allowed for a token.\n\t// After this many failed checks, the token is automatically deleted.\n\tmaxTries = 15\n)\n\n// Token represents a temporary token with TTL and arbitrary data.\ntype Token struct {\n\tTTL       time.Duration\n\tCreatedAt time.Time\n\tCount     int\n\tData      any\n}\n\nvar (\n\tErr = errors.New(\"token was not found or has expired\")\n\n\ttokens = make(map[string]Token)\n\tmu     sync.RWMutex\n)\n\nfunc init() {\n\t// Start periodic cleanup of expired temporary tokens (2FA, password reset).\n\tgo func() {\n\t\tticker := time.NewTicker(time.Hour)\n\t\tdefer ticker.Stop()\n\t\tfor range ticker.C {\n\t\t\tClean()\n\t\t}\n\t}()\n}\n\n// Set stores a token with the given ID, TTL, and data.\n// If a token with the same ID already exists, it will be overwritten silently.","sourceCodeStart":9,"sourceCodeEnd":45,"githubUrl":"https://github.com/knadh/listmonk/blob/670c01717d48647093335cc23a6be6f4b79c3b6b/internal/tmptokens/tmptokens.go#L9-L45","documentation":"Err is the sentinel error of the tmptokens package: temporary tokens (used for unsubscribe/previews/campaign archive access) are stored in an in-memory map with expiry and usage count. Lookup returns Err when the token is absent, expired, or its Count is exhausted. Callers surface it as invalid-link/404 style responses.","triggerScenarios":"Any handler resolving a token (GetServerConfig, GetCampaignArchivesFeed, CampaignArchivePage, getCampaignArchives, compileArchiveCampaigns) receives a token string not in the map, past TTL, or past its Count limit; server restarts wipe the in-memory map.","commonSituations":"User clicks an old archive/unsubscribe link after a listmonk restart (tokens are not persisted); link shared after its expiry window; exceeding the allowed views of a limited-use token; truncated/mistyped token URL.","solutions":["Regenerate the link (re-send the campaign or re-create the archive token) so a fresh token is issued.","Check whether the process restarted between issuance and use — in-memory tokens don't survive restarts; add external persistence if needed.","Increase the token TTL/Count configuration if links expire too quickly for your workflow.","Verify the URL contains the full, untruncated token (mail clients sometimes wrap/break long URLs)."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"tok, err := tmptokens.Get(token)\nif errors.Is(err, tmptokens.Err) {\n    http.Error(w, \"This link has expired or is invalid. Please request a new one.\", http.StatusNotFound)\n    return\n}","preventionTips":["Regenerate links after any listmonk restart — tokens are in-memory only.","Set token TTL/Count generous enough for real-world link-click delays.","Tell users to request fresh links instead of retrying old bookmarks.","Persist tokens externally (or re-issue on boot) if long-lived links are required."],"tags":["token","expiry","temporary-link","go"],"backgroundTag":"token-expired-or-not-found","analyzedSha":"670c01717d48647093335cc23a6be6f4b79c3b6b","analyzedAt":"2026-09-01T03:39:35.452Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}