mirror of
https://github.com/go-gitea/gitea.git
synced 2025-07-10 07:25:09 +02:00
Refactor mail template and support preview (#34990)
This commit is contained in:
parent
2cc3368610
commit
55f350542c
@ -9,6 +9,7 @@ import (
|
||||
"html/template"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
texttmpl "text/template"
|
||||
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
@ -16,6 +17,12 @@ import (
|
||||
"code.gitea.io/gitea/modules/util"
|
||||
)
|
||||
|
||||
type MailTemplates struct {
|
||||
TemplateNames []string
|
||||
BodyTemplates *template.Template
|
||||
SubjectTemplates *texttmpl.Template
|
||||
}
|
||||
|
||||
var mailSubjectSplit = regexp.MustCompile(`(?m)^-{3,}\s*$`)
|
||||
|
||||
// mailSubjectTextFuncMap returns functions for injecting to text templates, it's only used for mail subject
|
||||
@ -52,16 +59,17 @@ func buildSubjectBodyTemplate(stpl *texttmpl.Template, btpl *template.Template,
|
||||
return nil
|
||||
}
|
||||
|
||||
// Mailer provides the templates required for sending notification mails.
|
||||
func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) {
|
||||
subjectTemplates := texttmpl.New("")
|
||||
bodyTemplates := template.New("")
|
||||
|
||||
subjectTemplates.Funcs(mailSubjectTextFuncMap())
|
||||
bodyTemplates.Funcs(NewFuncMap())
|
||||
|
||||
// LoadMailTemplates provides the templates required for sending notification mails.
|
||||
func LoadMailTemplates(ctx context.Context, loadedTemplates *atomic.Pointer[MailTemplates]) {
|
||||
assetFS := AssetFS()
|
||||
refreshTemplates := func(firstRun bool) {
|
||||
var templateNames []string
|
||||
subjectTemplates := texttmpl.New("")
|
||||
bodyTemplates := template.New("")
|
||||
|
||||
subjectTemplates.Funcs(mailSubjectTextFuncMap())
|
||||
bodyTemplates.Funcs(NewFuncMap())
|
||||
|
||||
if !firstRun {
|
||||
log.Trace("Reloading mail templates")
|
||||
}
|
||||
@ -81,6 +89,7 @@ func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) {
|
||||
if firstRun {
|
||||
log.Trace("Adding mail template %s: %s by %s", tmplName, assetPath, layerName)
|
||||
}
|
||||
templateNames = append(templateNames, tmplName)
|
||||
if err = buildSubjectBodyTemplate(subjectTemplates, bodyTemplates, tmplName, content); err != nil {
|
||||
if firstRun {
|
||||
log.Fatal("Failed to parse mail template, err: %v", err)
|
||||
@ -88,6 +97,12 @@ func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) {
|
||||
log.Error("Failed to parse mail template, err: %v", err)
|
||||
}
|
||||
}
|
||||
loaded := &MailTemplates{
|
||||
TemplateNames: templateNames,
|
||||
BodyTemplates: bodyTemplates,
|
||||
SubjectTemplates: subjectTemplates,
|
||||
}
|
||||
loadedTemplates.Store(loaded)
|
||||
}
|
||||
|
||||
refreshTemplates(true)
|
||||
@ -99,6 +114,4 @@ func Mailer(ctx context.Context) (*texttmpl.Template, *template.Template) {
|
||||
refreshTemplates(false)
|
||||
})
|
||||
}
|
||||
|
||||
return subjectTemplates, bodyTemplates
|
||||
}
|
||||
|
58
routers/web/devtest/mail_preview.go
Normal file
58
routers/web/devtest/mail_preview.go
Normal file
@ -0,0 +1,58 @@
|
||||
// Copyright 2025 The Gitea Authors. All rights reserved.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package devtest
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/services/context"
|
||||
"code.gitea.io/gitea/services/mailer"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func MailPreviewRender(ctx *context.Context) {
|
||||
tmplName := ctx.PathParam("*")
|
||||
mockDataContent, err := templates.AssetFS().ReadFile("mail/" + tmplName + ".mock.yml")
|
||||
mockData := map[string]any{}
|
||||
if err == nil {
|
||||
err = yaml.Unmarshal(mockDataContent, &mockData)
|
||||
if err != nil {
|
||||
http.Error(ctx.Resp, "Failed to parse mock data: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
mockData["locale"] = ctx.Locale
|
||||
err = mailer.LoadedTemplates().BodyTemplates.ExecuteTemplate(ctx.Resp, tmplName, mockData)
|
||||
if err != nil {
|
||||
_, _ = ctx.Resp.Write([]byte(err.Error()))
|
||||
}
|
||||
}
|
||||
|
||||
func prepareMailPreviewRender(ctx *context.Context, tmplName string) {
|
||||
tmplSubject := mailer.LoadedTemplates().SubjectTemplates.Lookup(tmplName)
|
||||
if tmplSubject == nil {
|
||||
ctx.Data["RenderMailSubject"] = "default subject"
|
||||
} else {
|
||||
var buf strings.Builder
|
||||
err := tmplSubject.Execute(&buf, nil)
|
||||
if err != nil {
|
||||
ctx.Data["RenderMailSubject"] = err.Error()
|
||||
} else {
|
||||
ctx.Data["RenderMailSubject"] = buf.String()
|
||||
}
|
||||
}
|
||||
ctx.Data["RenderMailTemplateName"] = tmplName
|
||||
}
|
||||
|
||||
func MailPreview(ctx *context.Context) {
|
||||
ctx.Data["MailTemplateNames"] = mailer.LoadedTemplates().TemplateNames
|
||||
tmplName := ctx.FormString("tmpl")
|
||||
if tmplName != "" {
|
||||
prepareMailPreviewRender(ctx, tmplName)
|
||||
}
|
||||
ctx.HTML(http.StatusOK, "devtest/mail-preview")
|
||||
}
|
@ -1659,6 +1659,8 @@ func registerWebRoutes(m *web.Router) {
|
||||
m.Group("/devtest", func() {
|
||||
m.Any("", devtest.List)
|
||||
m.Any("/fetch-action-test", devtest.FetchActionTest)
|
||||
m.Any("/mail-preview", devtest.MailPreview)
|
||||
m.Any("/mail-preview/*", devtest.MailPreviewRender)
|
||||
m.Any("/{sub}", devtest.TmplCommon)
|
||||
m.Get("/repo-action-view/{run}/{job}", devtest.MockActionsView)
|
||||
m.Post("/actions-mock/runs/{run}/jobs/{job}", web.Bind(actions.ViewRequest{}), devtest.MockActionsRunsJobs)
|
||||
|
@ -15,7 +15,7 @@ import (
|
||||
"mime"
|
||||
"regexp"
|
||||
"strings"
|
||||
texttmpl "text/template"
|
||||
"sync/atomic"
|
||||
|
||||
repo_model "code.gitea.io/gitea/models/repo"
|
||||
user_model "code.gitea.io/gitea/models/user"
|
||||
@ -23,6 +23,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/log"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/typesniffer"
|
||||
sender_service "code.gitea.io/gitea/services/mailer/sender"
|
||||
|
||||
@ -31,11 +32,13 @@ import (
|
||||
|
||||
const mailMaxSubjectRunes = 256 // There's no actual limit for subject in RFC 5322
|
||||
|
||||
var (
|
||||
bodyTemplates *template.Template
|
||||
subjectTemplates *texttmpl.Template
|
||||
subjectRemoveSpaces = regexp.MustCompile(`[\s]+`)
|
||||
)
|
||||
var loadedTemplates atomic.Pointer[templates.MailTemplates]
|
||||
|
||||
var subjectRemoveSpaces = regexp.MustCompile(`[\s]+`)
|
||||
|
||||
func LoadedTemplates() *templates.MailTemplates {
|
||||
return loadedTemplates.Load()
|
||||
}
|
||||
|
||||
// SendTestMail sends a test mail
|
||||
func SendTestMail(email string) error {
|
||||
|
@ -119,7 +119,7 @@ func composeIssueCommentMessages(ctx context.Context, comment *mailComment, lang
|
||||
}
|
||||
|
||||
var mailSubject bytes.Buffer
|
||||
if err := subjectTemplates.ExecuteTemplate(&mailSubject, tplName, mailMeta); err == nil {
|
||||
if err := LoadedTemplates().SubjectTemplates.ExecuteTemplate(&mailSubject, tplName, mailMeta); err == nil {
|
||||
subject = sanitizeSubject(mailSubject.String())
|
||||
if subject == "" {
|
||||
subject = fallback
|
||||
@ -134,7 +134,7 @@ func composeIssueCommentMessages(ctx context.Context, comment *mailComment, lang
|
||||
|
||||
var mailBody bytes.Buffer
|
||||
|
||||
if err := bodyTemplates.ExecuteTemplate(&mailBody, tplName, mailMeta); err != nil {
|
||||
if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&mailBody, tplName, mailMeta); err != nil {
|
||||
log.Error("ExecuteTemplate [%s]: %v", tplName+"/body", err)
|
||||
}
|
||||
|
||||
@ -260,14 +260,14 @@ func actionToTemplate(issue *issues_model.Issue, actionType activities_model.Act
|
||||
}
|
||||
|
||||
template = typeName + "/" + name
|
||||
ok := bodyTemplates.Lookup(template) != nil
|
||||
ok := LoadedTemplates().BodyTemplates.Lookup(template) != nil
|
||||
if !ok && typeName != "issue" {
|
||||
template = "issue/" + name
|
||||
ok = bodyTemplates.Lookup(template) != nil
|
||||
ok = LoadedTemplates().BodyTemplates.Lookup(template) != nil
|
||||
}
|
||||
if !ok {
|
||||
template = typeName + "/default"
|
||||
ok = bodyTemplates.Lookup(template) != nil
|
||||
ok = LoadedTemplates().BodyTemplates.Lookup(template) != nil
|
||||
}
|
||||
if !ok {
|
||||
template = "issue/default"
|
||||
|
@ -79,7 +79,7 @@ func mailNewRelease(ctx context.Context, lang string, tos []*user_model.User, re
|
||||
|
||||
var mailBody bytes.Buffer
|
||||
|
||||
if err := bodyTemplates.ExecuteTemplate(&mailBody, string(tplNewReleaseMail), mailMeta); err != nil {
|
||||
if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&mailBody, string(tplNewReleaseMail), mailMeta); err != nil {
|
||||
log.Error("ExecuteTemplate [%s]: %v", string(tplNewReleaseMail)+"/body", err)
|
||||
return
|
||||
}
|
||||
|
@ -78,7 +78,7 @@ func sendRepoTransferNotifyMailPerLang(lang string, newOwner, doer *user_model.U
|
||||
"Destination": destination,
|
||||
}
|
||||
|
||||
if err := bodyTemplates.ExecuteTemplate(&content, string(mailRepoTransferNotify), data); err != nil {
|
||||
if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&content, string(mailRepoTransferNotify), data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
@ -62,7 +62,7 @@ func MailTeamInvite(ctx context.Context, inviter *user_model.User, team *org_mod
|
||||
}
|
||||
|
||||
var mailBody bytes.Buffer
|
||||
if err := bodyTemplates.ExecuteTemplate(&mailBody, string(tplTeamInviteMail), mailMeta); err != nil {
|
||||
if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&mailBody, string(tplTeamInviteMail), mailMeta); err != nil {
|
||||
log.Error("ExecuteTemplate [%s]: %v", string(tplTeamInviteMail)+"/body", err)
|
||||
return err
|
||||
}
|
||||
|
@ -25,6 +25,7 @@ import (
|
||||
"code.gitea.io/gitea/modules/markup"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"code.gitea.io/gitea/modules/storage"
|
||||
"code.gitea.io/gitea/modules/templates"
|
||||
"code.gitea.io/gitea/modules/test"
|
||||
"code.gitea.io/gitea/services/attachment"
|
||||
sender_service "code.gitea.io/gitea/services/mailer/sender"
|
||||
@ -95,6 +96,13 @@ func prepareMailerBase64Test(t *testing.T) (doer *user_model.User, repo *repo_mo
|
||||
return user, repo, issue, att1, att2
|
||||
}
|
||||
|
||||
func prepareMailTemplates(name, subjectTmpl, bodyTmpl string) {
|
||||
loadedTemplates.Store(&templates.MailTemplates{
|
||||
SubjectTemplates: texttmpl.Must(texttmpl.New(name).Parse(subjectTmpl)),
|
||||
BodyTemplates: template.Must(template.New(name).Parse(bodyTmpl)),
|
||||
})
|
||||
}
|
||||
|
||||
func TestComposeIssueComment(t *testing.T) {
|
||||
doer, _, issue, comment := prepareMailerTest(t)
|
||||
|
||||
@ -107,8 +115,7 @@ func TestComposeIssueComment(t *testing.T) {
|
||||
setting.IncomingEmail.Enabled = true
|
||||
defer func() { setting.IncomingEmail.Enabled = false }()
|
||||
|
||||
subjectTemplates = texttmpl.Must(texttmpl.New("issue/comment").Parse(subjectTpl))
|
||||
bodyTemplates = template.Must(template.New("issue/comment").Parse(bodyTpl))
|
||||
prepareMailTemplates("issue/comment", subjectTpl, bodyTpl)
|
||||
|
||||
recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}, {Name: "Test2", Email: "test2@gitea.com"}}
|
||||
msgs, err := composeIssueCommentMessages(t.Context(), &mailComment{
|
||||
@ -153,8 +160,7 @@ func TestComposeIssueComment(t *testing.T) {
|
||||
func TestMailMentionsComment(t *testing.T) {
|
||||
doer, _, issue, comment := prepareMailerTest(t)
|
||||
comment.Poster = doer
|
||||
subjectTemplates = texttmpl.Must(texttmpl.New("issue/comment").Parse(subjectTpl))
|
||||
bodyTemplates = template.Must(template.New("issue/comment").Parse(bodyTpl))
|
||||
prepareMailTemplates("issue/comment", subjectTpl, bodyTpl)
|
||||
mails := 0
|
||||
|
||||
defer test.MockVariableValue(&SendAsync, func(msgs ...*sender_service.Message) {
|
||||
@ -169,9 +175,7 @@ func TestMailMentionsComment(t *testing.T) {
|
||||
func TestComposeIssueMessage(t *testing.T) {
|
||||
doer, _, issue, _ := prepareMailerTest(t)
|
||||
|
||||
subjectTemplates = texttmpl.Must(texttmpl.New("issue/new").Parse(subjectTpl))
|
||||
bodyTemplates = template.Must(template.New("issue/new").Parse(bodyTpl))
|
||||
|
||||
prepareMailTemplates("issue/new", subjectTpl, bodyTpl)
|
||||
recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}, {Name: "Test2", Email: "test2@gitea.com"}}
|
||||
msgs, err := composeIssueCommentMessages(t.Context(), &mailComment{
|
||||
Issue: issue, Doer: doer, ActionType: activities_model.ActionCreateIssue,
|
||||
@ -200,15 +204,14 @@ func TestTemplateSelection(t *testing.T) {
|
||||
doer, repo, issue, comment := prepareMailerTest(t)
|
||||
recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}}
|
||||
|
||||
subjectTemplates = texttmpl.Must(texttmpl.New("issue/default").Parse("issue/default/subject"))
|
||||
texttmpl.Must(subjectTemplates.New("issue/new").Parse("issue/new/subject"))
|
||||
texttmpl.Must(subjectTemplates.New("pull/comment").Parse("pull/comment/subject"))
|
||||
texttmpl.Must(subjectTemplates.New("issue/close").Parse("")) // Must default to fallback subject
|
||||
prepareMailTemplates("issue/default", "issue/default/subject", "issue/default/body")
|
||||
|
||||
bodyTemplates = template.Must(template.New("issue/default").Parse("issue/default/body"))
|
||||
template.Must(bodyTemplates.New("issue/new").Parse("issue/new/body"))
|
||||
template.Must(bodyTemplates.New("pull/comment").Parse("pull/comment/body"))
|
||||
template.Must(bodyTemplates.New("issue/close").Parse("issue/close/body"))
|
||||
texttmpl.Must(LoadedTemplates().SubjectTemplates.New("issue/new").Parse("issue/new/subject"))
|
||||
texttmpl.Must(LoadedTemplates().SubjectTemplates.New("pull/comment").Parse("pull/comment/subject"))
|
||||
texttmpl.Must(LoadedTemplates().SubjectTemplates.New("issue/close").Parse("")) // Must default to a fallback subject
|
||||
template.Must(LoadedTemplates().BodyTemplates.New("issue/new").Parse("issue/new/body"))
|
||||
template.Must(LoadedTemplates().BodyTemplates.New("pull/comment").Parse("pull/comment/body"))
|
||||
template.Must(LoadedTemplates().BodyTemplates.New("issue/close").Parse("issue/close/body"))
|
||||
|
||||
expect := func(t *testing.T, msg *sender_service.Message, expSubject, expBody string) {
|
||||
subject := msg.ToMessage().GetGenHeader("Subject")
|
||||
@ -253,9 +256,7 @@ func TestTemplateServices(t *testing.T) {
|
||||
expect := func(t *testing.T, issue *issues_model.Issue, comment *issues_model.Comment, doer *user_model.User,
|
||||
actionType activities_model.ActionType, fromMention bool, tplSubject, tplBody, expSubject, expBody string,
|
||||
) {
|
||||
subjectTemplates = texttmpl.Must(texttmpl.New("issue/default").Parse(tplSubject))
|
||||
bodyTemplates = template.Must(template.New("issue/default").Parse(tplBody))
|
||||
|
||||
prepareMailTemplates("issue/default", tplSubject, tplBody)
|
||||
recipients := []*user_model.User{{Name: "Test", Email: "test@gitea.com"}}
|
||||
msg := testComposeIssueCommentMessage(t, &mailComment{
|
||||
Issue: issue, Doer: doer, ActionType: actionType,
|
||||
@ -512,8 +513,7 @@ func TestEmbedBase64Images(t *testing.T) {
|
||||
att2ImgBase64 := fmt.Sprintf(`<img src="%s"/>`, att2Base64)
|
||||
|
||||
t.Run("ComposeMessage", func(t *testing.T) {
|
||||
subjectTemplates = texttmpl.Must(texttmpl.New("issue/new").Parse(subjectTpl))
|
||||
bodyTemplates = template.Must(template.New("issue/new").Parse(bodyTpl))
|
||||
prepareMailTemplates("issue/new", subjectTpl, bodyTpl)
|
||||
|
||||
issue.Content = fmt.Sprintf(`MSG-BEFORE <image src="attachments/%s"> MSG-AFTER`, att1.UUID)
|
||||
require.NoError(t, issues_model.UpdateIssueCols(t.Context(), issue, "content"))
|
||||
|
@ -39,7 +39,7 @@ func sendUserMail(language string, u *user_model.User, tpl templates.TplName, co
|
||||
|
||||
var content bytes.Buffer
|
||||
|
||||
if err := bodyTemplates.ExecuteTemplate(&content, string(tpl), data); err != nil {
|
||||
if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&content, string(tpl), data); err != nil {
|
||||
log.Error("Template: %v", err)
|
||||
return
|
||||
}
|
||||
@ -90,7 +90,7 @@ func SendActivateEmailMail(u *user_model.User, email string) {
|
||||
|
||||
var content bytes.Buffer
|
||||
|
||||
if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuthActivateEmail), data); err != nil {
|
||||
if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&content, string(mailAuthActivateEmail), data); err != nil {
|
||||
log.Error("Template: %v", err)
|
||||
return
|
||||
}
|
||||
@ -118,7 +118,7 @@ func SendRegisterNotifyMail(u *user_model.User) {
|
||||
|
||||
var content bytes.Buffer
|
||||
|
||||
if err := bodyTemplates.ExecuteTemplate(&content, string(mailAuthRegisterNotify), data); err != nil {
|
||||
if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&content, string(mailAuthRegisterNotify), data); err != nil {
|
||||
log.Error("Template: %v", err)
|
||||
return
|
||||
}
|
||||
@ -149,7 +149,7 @@ func SendCollaboratorMail(u, doer *user_model.User, repo *repo_model.Repository)
|
||||
|
||||
var content bytes.Buffer
|
||||
|
||||
if err := bodyTemplates.ExecuteTemplate(&content, string(mailNotifyCollaborator), data); err != nil {
|
||||
if err := LoadedTemplates().BodyTemplates.ExecuteTemplate(&content, string(mailNotifyCollaborator), data); err != nil {
|
||||
log.Error("Template: %v", err)
|
||||
return
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ func NewContext(ctx context.Context) {
|
||||
sender = &sender_service.SMTPSender{}
|
||||
}
|
||||
|
||||
subjectTemplates, bodyTemplates = templates.Mailer(ctx)
|
||||
templates.LoadMailTemplates(ctx, &loadedTemplates)
|
||||
|
||||
mailQueue = queue.CreateSimpleQueue(graceful.GetManager().ShutdownContext(), "mail", func(items ...*sender_service.Message) []*sender_service.Message {
|
||||
for _, msg := range items {
|
||||
|
@ -1,2 +1,3 @@
|
||||
{{template "base/head" ctx.RootData}}
|
||||
<link rel="stylesheet" href="{{AssetUrlPrefix}}/css/devtest.css?v={{AssetVersion}}">
|
||||
{{template "base/alert" .}}
|
||||
|
27
templates/devtest/mail-preview.tmpl
Normal file
27
templates/devtest/mail-preview.tmpl
Normal file
@ -0,0 +1,27 @@
|
||||
{{template "devtest/devtest-header"}}
|
||||
<div class="page-content devtest ui container">
|
||||
<div class="flex-text-block tw-flex-wrap">
|
||||
{{range $templateName := .MailTemplateNames}}
|
||||
<a class="ui button" href="?tmpl={{$templateName}}">{{$templateName}}</a>
|
||||
{{else}}
|
||||
<p>Mailer service is not enabled or no template is found</p>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{if .RenderMailTemplateName}}
|
||||
<div class="tw-my-2">
|
||||
<div>Preview of: {{.RenderMailTemplateName}}</div>
|
||||
<div>Subject: {{.RenderMailSubject}}</div>
|
||||
<iframe src="{{AppSubUrl}}/devtest/mail-preview/{{.RenderMailTemplateName}}" class="mail-preview-body"></iframe>
|
||||
<style>
|
||||
.mail-preview-body {
|
||||
border: 1px solid #ccc;
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{template "devtest/devtest-footer"}}
|
3
templates/mail/auth/activate.mock.yml
Normal file
3
templates/mail/auth/activate.mock.yml
Normal file
@ -0,0 +1,3 @@
|
||||
DisplayName: User Display Name
|
||||
Code: The-Activation-Code
|
||||
ActiveCodeLives: 24h
|
Loading…
x
Reference in New Issue
Block a user