diff --git a/.vscode/settings.json b/.vscode/settings.json
index b7d39e6e5..63b0a94c1 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -5,5 +5,13 @@
"explorer.autoReveal": "focusNoScroll",
"cSpell.words": [
"grecaptcha"
- ]
+ ],
+ "go.goroot": "${env:USERPROFILE}\\tools\\go",
+ "go.gopath": "${env:USERPROFILE}\\go",
+ "terminal.integrated.env.windows": {
+ "PATH": "${env:USERPROFILE}\\tools\\go\\bin;${env:USERPROFILE}\\tools\\node-v22.16.0-win-x64;${env:PATH}",
+ "GOROOT": "${env:USERPROFILE}\\tools\\go",
+ "GOPATH": "${env:USERPROFILE}\\go"
+ },
+ "go.toolsManagement.autoUpdate": true
}
diff --git a/GSOC.md b/GSOC.md
new file mode 100644
index 000000000..cd8995dbd
--- /dev/null
+++ b/GSOC.md
@@ -0,0 +1,95 @@
+# GSoC 2026 — Apache Answer Contribution Log
+
+> **Contributor:** Yash Chauhan (@Yash-Chauhan22)
+> **Upstream Project:** [apache/answer](https://github.com/apache/answer)
+> **Fork:** [Yash-Chauhan22/answer](https://github.com/Yash-Chauhan22/answer)
+> **Program:** Google Summer of Code 2026
+
+---
+
+## Repository Setup
+
+| Task | Status |
+|------|--------|
+| Fork apache/answer | Done |
+| Clone fork locally | Done |
+| Add upstream remote | Done |
+| Install Go toolchain | In progress |
+| Install Node.js + pnpm | In progress |
+| Create .env config | Done |
+| Create gsoc-dev branch | Pending |
+
+---
+
+## Git Remotes
+
+`
+origin -> https://github.com/Yash-Chauhan22/answer.git (your fork)
+upstream -> https://github.com/apache/answer.git (main project)
+`
+
+### Syncing with upstream
+
+`bash
+git fetch upstream
+git checkout main
+git merge upstream/main
+git push origin main
+`
+
+---
+
+## Branch Strategy
+
+| Branch | Purpose |
+|--------|---------|
+| main | Mirrors upstream apache/answer main |
+| gsoc-dev | Active GSoC development base |
+| feature/xxx | Individual feature branches |
+
+---
+
+## Running the Project
+
+### Option 1: Docker (Quickest)
+
+`bash
+docker-compose up -d
+# Access at http://localhost:9080
+`
+
+### Option 2: Full Local Dev
+
+Backend:
+`bash
+go mod download
+go run ./cmd/answer/... run -C ./configs/
+`
+
+Frontend (new terminal):
+`bash
+cd ui
+pnpm install
+pnpm dev
+`
+
+---
+
+## Important Links
+
+| Resource | URL |
+|----------|-----|
+| Project Website | https://answer.apache.org |
+| Upstream GitHub | https://github.com/apache/answer |
+| Contributing Guide | https://answer.apache.org/community/contributing |
+| Discord | https://discord.gg/a6PZZbfnFx |
+| Dev Mailing List | dev@answer.apache.org |
+
+---
+
+## Contribution Notes
+
+- PRs go to apache/answer (upstream), not your fork
+- Each PR needs an associated GitHub issue
+- Conventional commits: feat:, fix:, docs:, refactor:, test:
+- Apache License header required on new files (CI checks this)
diff --git a/docker-compose.yaml b/docker-compose.yaml
index 58e8ea036..3d67e922a 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -15,7 +15,6 @@
# specific language governing permissions and limitations
# under the License.
-version: "3"
services:
answer:
image: apache/answer
diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml
index 9a0d198b3..31a5bc10a 100644
--- a/i18n/en_US.yaml
+++ b/i18n/en_US.yaml
@@ -487,42 +487,42 @@ backend:
title:
other: "[{{.SiteName}}] Confirm your new email address"
body:
- other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}
\n\nIf you did not request this change, please ignore this email.
\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen."
+ other: "
Confirm your new email address for {{.SiteName}} by clicking on the following link:
\n{{.ChangeEmailUrl}}
\n\nIf you did not request this change, please ignore this email.
\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen."
new_answer:
title:
other: "[{{.SiteName}}] {{.DisplayName}} answered your question"
body:
- other: "{{.QuestionTitle}}
\n\n{{.DisplayName}}:
\n{{.AnswerSummary}}
\nView it on {{.SiteName}}
\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.
\n\nUnsubscribe"
+ other: "{{.QuestionTitle}}
\n\n{{.DisplayName}}:
\n{{.AnswerSummary}}
\nView it on {{.SiteName}}
\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.
\n\nUnsubscribe"
invited_you_to_answer:
title:
other: "[{{.SiteName}}] {{.DisplayName}} invited you to answer"
body:
- other: "{{.QuestionTitle}}
\n\n{{.DisplayName}}:
\nI think you may know the answer.
\nView it on {{.SiteName}}
\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.
\n\nUnsubscribe"
+ other: "{{.QuestionTitle}}
\n\n{{.DisplayName}}:
\nI think you may know the answer.
\nView it on {{.SiteName}}
\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.
\n\nUnsubscribe"
new_comment:
title:
other: "[{{.SiteName}}] {{.DisplayName}} commented on your post"
body:
- other: "{{.QuestionTitle}}
\n\n{{.DisplayName}}:
\n{{.CommentSummary}}
\nView it on {{.SiteName}}
\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.
\n\nUnsubscribe"
+ other: "{{.QuestionTitle}}
\n\n{{.DisplayName}}:
\n{{.CommentSummary}}
\nView it on {{.SiteName}}
\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.
\n\nUnsubscribe"
new_question:
title:
other: "[{{.SiteName}}] New question: {{.QuestionTitle}}"
body:
- other: "{{.QuestionTitle}}
\n{{.Tags}}
\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.
\n\nUnsubscribe"
+ other: "{{.QuestionTitle}}
\n{{.Tags}}
\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.
\n\nUnsubscribe"
pass_reset:
title:
other: "[{{.SiteName }}] Password reset"
body:
- other: "Somebody asked to reset your password on {{.SiteName}}.
\n\nIf it was not you, you can safely ignore this email.
\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n
\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen."
+ other: "Somebody asked to reset your password on {{.SiteName}}.
\n\nIf it was not you, you can safely ignore this email.
\n\nClick the following link to choose a new password:
\n{{.PassResetUrl}}\n
\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen."
register:
title:
other: "[{{.SiteName}}] Confirm your new account"
body:
- other: "Welcome to {{.SiteName}}!
\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}
\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n
\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen."
+ other: "Welcome to {{.SiteName}}!
\n\nClick the following link to confirm and activate your new account:
\n{{.RegisterUrl}}
\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n
\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen."
test:
title:
other: "[{{.SiteName}}] Test Email"
body:
- other: "This is a test email.\n
\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen."
+ other: "This is a test email.\n
\n\n--
\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen."
action_activity_type:
upvote:
other: upvote
diff --git a/internal/service/export/email_service.go b/internal/service/export/email_service.go
index bb00b828b..63c8f087b 100644
--- a/internal/service/export/email_service.go
+++ b/internal/service/export/email_service.go
@@ -44,20 +44,20 @@ import (
"gopkg.in/gomail.v2"
)
-// EmailService kit service
+// EmailService handles email composition and delivery.
type EmailService struct {
configService *config.ConfigService
emailRepo EmailRepo
siteInfoService siteinfo_common.SiteInfoCommonService
}
-// EmailRepo email repository
+// EmailRepo defines the interface for storing and verifying email verification codes.
type EmailRepo interface {
SetCode(ctx context.Context, userID, code, content string, duration time.Duration) error
VerifyCode(ctx context.Context, code string) (content string, err error)
}
-// NewEmailService email service
+// NewEmailService creates a new EmailService instance.
func NewEmailService(
configService *config.ConfigService,
emailRepo EmailRepo,
@@ -70,7 +70,7 @@ func NewEmailService(
}
}
-// EmailConfig email config
+// EmailConfig holds SMTP email configuration settings.
type EmailConfig struct {
FromEmail string `json:"from_email"`
FromName string `json:"from_name"`
@@ -90,7 +90,7 @@ func (e *EmailConfig) IsTLS() bool {
return e.Encryption == "TLS"
}
-// SaveCode save code
+// SaveCode stores a verification code in the cache for the given user.
func (es *EmailService) SaveCode(ctx context.Context, userID, code, codeContent string) {
err := es.emailRepo.SetCode(ctx, userID, code, codeContent, constant.UserEmailCodeCacheTime)
if err != nil {
@@ -98,7 +98,7 @@ func (es *EmailService) SaveCode(ctx context.Context, userID, code, codeContent
}
}
-// SendAndSaveCode send email and save code
+// SendAndSaveCode saves a verification code and sends the corresponding email to the recipient.
func (es *EmailService) SendAndSaveCode(ctx context.Context, userID, toEmailAddr, subject, body, code, codeContent string) {
err := es.emailRepo.SetCode(ctx, userID, code, codeContent, constant.UserEmailCodeCacheTime)
if err != nil {
@@ -108,7 +108,7 @@ func (es *EmailService) SendAndSaveCode(ctx context.Context, userID, toEmailAddr
es.Send(ctx, toEmailAddr, subject, body)
}
-// SendAndSaveCodeWithTime send email and save code
+// SendAndSaveCodeWithTime saves a verification code with a custom expiry duration and sends the email.
func (es *EmailService) SendAndSaveCodeWithTime(
ctx context.Context, userID, toEmailAddr, subject, body, code, codeContent string, duration time.Duration) {
err := es.emailRepo.SetCode(ctx, userID, code, codeContent, duration)
@@ -119,7 +119,7 @@ func (es *EmailService) SendAndSaveCodeWithTime(
es.Send(ctx, toEmailAddr, subject, body)
}
-// Send email send
+// Send sends an HTML email to the specified recipient via SMTP.
func (es *EmailService) Send(ctx context.Context, toEmailAddr, subject, body string) {
log.Infof("try to send email to %s", toEmailAddr)
ec, err := es.GetEmailConfig(ctx)
@@ -137,7 +137,8 @@ func (es *EmailService) Send(ctx context.Context, toEmailAddr, subject, body str
m.SetHeader("From", fmt.Sprintf("%s <%s>", fromName, ec.FromEmail))
m.SetHeader("To", toEmailAddr)
m.SetHeader("Subject", subject)
- m.SetBody("text/html", body)
+ m.SetHeader("MIME-Version", "1.0")
+ m.SetBody("text/html; charset=UTF-8", body)
d := gomail.NewDialer(ec.SMTPHost, ec.SMTPPort, ec.SMTPUsername, ec.SMTPPassword)
if ec.IsSSL() {
@@ -156,7 +157,7 @@ func (es *EmailService) Send(ctx context.Context, toEmailAddr, subject, body str
}
}
-// VerifyUrlExpired email send
+// VerifyUrlExpired checks whether the verification URL associated with the given code has expired.
func (es *EmailService) VerifyUrlExpired(ctx context.Context, code string) (content string) {
content, err := es.emailRepo.VerifyCode(ctx, code)
if err != nil {
@@ -211,7 +212,7 @@ func (es *EmailService) ChangeEmailTemplate(ctx context.Context, changeEmailUrl
return title, body, nil
}
-// TestTemplate send test email template parse
+// TestTemplate generates the title and body for a test email.
func (es *EmailService) TestTemplate(ctx context.Context) (title, body string, err error) {
siteInfo, err := es.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
@@ -229,7 +230,7 @@ func escapeEmailHTMLText(text string) string {
return html.EscapeString(text)
}
-// NewAnswerTemplate new answer template
+// NewAnswerTemplate generates the email title and body notifying a user their question received a new answer.
func (es *EmailService) NewAnswerTemplate(ctx context.Context, raw *schema.NewAnswerTemplateRawData) (
title, body string, err error) {
siteInfo, err := es.siteInfoService.GetSiteGeneral(ctx)
@@ -262,7 +263,7 @@ func (es *EmailService) NewAnswerTemplate(ctx context.Context, raw *schema.NewAn
return title, body, nil
}
-// NewInviteAnswerTemplate new invite answer template
+// NewInviteAnswerTemplate generates the email title and body for an invitation to answer a question.
func (es *EmailService) NewInviteAnswerTemplate(ctx context.Context, raw *schema.NewInviteAnswerTemplateRawData) (
title, body string, err error) {
siteInfo, err := es.siteInfoService.GetSiteGeneral(ctx)
@@ -293,7 +294,7 @@ func (es *EmailService) NewInviteAnswerTemplate(ctx context.Context, raw *schema
return title, body, nil
}
-// NewCommentTemplate new comment template
+// NewCommentTemplate generates the email title and body notifying a user of a new comment on their post.
func (es *EmailService) NewCommentTemplate(ctx context.Context, raw *schema.NewCommentTemplateRawData) (
title, body string, err error) {
siteInfo, err := es.siteInfoService.GetSiteGeneral(ctx)
@@ -327,7 +328,7 @@ func (es *EmailService) NewCommentTemplate(ctx context.Context, raw *schema.NewC
return title, body, nil
}
-// NewQuestionTemplate new question template
+// NewQuestionTemplate generates the email title and body notifying subscribers of a new question.
func (es *EmailService) NewQuestionTemplate(ctx context.Context, raw *schema.NewQuestionTemplateRawData) (
title, body string, err error) {
siteInfo, err := es.siteInfoService.GetSiteGeneral(ctx)
@@ -373,7 +374,7 @@ func (es *EmailService) GetEmailConfig(ctx context.Context) (ec *EmailConfig, er
return ec, nil
}
-// SetEmailConfig set email config
+// SetEmailConfig persists the email SMTP configuration.
func (es *EmailService) SetEmailConfig(ctx context.Context, ec *EmailConfig) (err error) {
data, _ := json.Marshal(ec)
return es.configService.UpdateConfig(ctx, constant.EmailConfigKey, string(data))