From c08e3caab65fbd85204bbc93231065c6481b1b7b Mon Sep 17 00:00:00 2001 From: Yash Chauhan Date: Mon, 29 Jun 2026 15:21:20 +0530 Subject: [PATCH 1/3] chore: GSoC dev environment setup - Add .env from template for local development - Add GSOC.md contribution tracker and reference guide - Update .vscode/settings.json with Go/Node portable paths - Remove obsolete version field from docker-compose.yaml --- .vscode/settings.json | 10 ++++- GSOC.md | 95 +++++++++++++++++++++++++++++++++++++++++++ docker-compose.yaml | 1 - 3 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 GSOC.md 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 From 93879e123744b484996fd5ae194cb680ef53860e Mon Sep 17 00:00:00 2001 From: Yash Chauhan Date: Mon, 29 Jun 2026 16:18:53 +0530 Subject: [PATCH 2/3] fix: add proper HTML structure to email templates to reduce spam score Fixes #783 Emails sent by Answer were being flagged by SpamAssassin with the TVD_PH_BODY_META_ALL rule due to missing HTML document structure (DOCTYPE, html, head, meta charset tags) in the email body. Changes: - Add MIME-Version header and explicit UTF-8 charset to email sender - Wrap all 8 email body templates (en_US) with proper HTML document structure including DOCTYPE declaration, meta charset=UTF-8, and viewport meta tag This ensures email clients and spam filters correctly identify the content type and encoding, reducing the spam score. --- i18n/en_US.yaml | 16 ++++++++-------- internal/service/export/email_service.go | 3 ++- 2 files changed, 10 insertions(+), 9 deletions(-) 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}}:
\n
I 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}}:
\n
I 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..0524f841c 100644 --- a/internal/service/export/email_service.go +++ b/internal/service/export/email_service.go @@ -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() { From 506ece3f918b40dd642d88d30a77d0fea240ff91 Mon Sep 17 00:00:00 2001 From: Yash Chauhan Date: Mon, 29 Jun 2026 16:28:35 +0530 Subject: [PATCH 3/3] docs: improve Go doc comments in email_service.go - Fix incorrect comment on VerifyUrlExpired (was 'email send', now accurately describes URL expiry verification) - Replace vague 'kit service', 'email config', 'save code' etc. with clear, descriptive sentences following Go doc conventions - All 15 exported symbols now have accurate, meaningful documentation --- internal/service/export/email_service.go | 30 ++++++++++++------------ 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/internal/service/export/email_service.go b/internal/service/export/email_service.go index 0524f841c..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) @@ -157,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 { @@ -212,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 { @@ -230,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) @@ -263,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) @@ -294,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) @@ -328,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) @@ -374,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))