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}}:
\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..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))