Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
95 changes: 95 additions & 0 deletions GSOC.md
Original file line number Diff line number Diff line change
@@ -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)
1 change: 0 additions & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
# specific language governing permissions and limitations
# under the License.

version: "3"
services:
answer:
image: apache/answer
Expand Down
16 changes: 8 additions & 8 deletions i18n/en_US.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:<br>\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\n\nIf you did not request this change, please ignore this email.<br><br>\n\n--<br>\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen."
other: "<!DOCTYPE html><html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width'></head><body>Confirm your new email address for {{.SiteName}} by clicking on the following link:<br>\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\n\nIf you did not request this change, please ignore this email.<br><br>\n\n--<br>\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.</body></html>"
new_answer:
title:
other: "[{{.SiteName}}] {{.DisplayName}} answered your question"
body:
other: "<a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a><br><br>\n\n{{.DisplayName}}:<br>\n<blockquote>{{.AnswerSummary}}</blockquote><br>\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n--<br>\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\n\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>"
other: "<!DOCTYPE html><html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width'></head><body><a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a><br><br>\n\n{{.DisplayName}}:<br>\n<blockquote>{{.AnswerSummary}}</blockquote><br>\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n--<br>\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\n\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small></body></html>"
invited_you_to_answer:
title:
other: "[{{.SiteName}}] {{.DisplayName}} invited you to answer"
body:
other: "<a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a><br><br>\n\n{{.DisplayName}}:<br>\n<blockquote>I think you may know the answer.</blockquote><br>\n<a href='{{.InviteUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n--<br>\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\n\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>"
other: "<!DOCTYPE html><html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width'></head><body><a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a><br><br>\n\n{{.DisplayName}}:<br>\n<blockquote>I think you may know the answer.</blockquote><br>\n<a href='{{.InviteUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n--<br>\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\n\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small></body></html>"
new_comment:
title:
other: "[{{.SiteName}}] {{.DisplayName}} commented on your post"
body:
other: "<a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a><br><br>\n\n{{.DisplayName}}:<br>\n<blockquote>{{.CommentSummary}}</blockquote><br>\n<a href='{{.CommentUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n--<br>\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\n\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>"
other: "<!DOCTYPE html><html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width'></head><body><a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a><br><br>\n\n{{.DisplayName}}:<br>\n<blockquote>{{.CommentSummary}}</blockquote><br>\n<a href='{{.CommentUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n--<br>\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\n\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small></body></html>"
new_question:
title:
other: "[{{.SiteName}}] New question: {{.QuestionTitle}}"
body:
other: "<a href='{{.QuestionUrl}}'>{{.QuestionTitle}}</a><br>\n<small>{{.Tags}}</small><br><br>\n\n--<br>\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\n\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>"
other: "<!DOCTYPE html><html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width'></head><body><a href='{{.QuestionUrl}}'>{{.QuestionTitle}}</a><br>\n<small>{{.Tags}}</small><br><br>\n\n--<br>\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.<br><br>\n\n<small><a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small></body></html>"
pass_reset:
title:
other: "[{{.SiteName }}] Password reset"
body:
other: "Somebody asked to reset your password on {{.SiteName}}.<br><br>\n\nIf it was not you, you can safely ignore this email.<br><br>\n\nClick the following link to choose a new password:<br>\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\n<br><br>\n\n--<br>\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen."
other: "<!DOCTYPE html><html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width'></head><body>Somebody asked to reset your password on {{.SiteName}}.<br><br>\n\nIf it was not you, you can safely ignore this email.<br><br>\n\nClick the following link to choose a new password:<br>\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\n<br><br>\n\n--<br>\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.</body></html>"
register:
title:
other: "[{{.SiteName}}] Confirm your new account"
body:
other: "Welcome to {{.SiteName}}!<br><br>\n\nClick the following link to confirm and activate your new account:<br>\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n<br><br>\n\n--<br>\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen."
other: "<!DOCTYPE html><html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width'></head><body>Welcome to {{.SiteName}}!<br><br>\n\nClick the following link to confirm and activate your new account:<br>\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n<br><br>\n\n--<br>\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.</body></html>"
test:
title:
other: "[{{.SiteName}}] Test Email"
body:
other: "This is a test email.\n<br><br>\n\n--<br>\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen."
other: "<!DOCTYPE html><html><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width'></head><body>This is a test email.\n<br><br>\n\n--<br>\nNote: This is an automatic system email, please do not reply to this message as your response will not be seen.</body></html>"
action_activity_type:
upvote:
other: upvote
Expand Down
33 changes: 17 additions & 16 deletions internal/service/export/email_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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"`
Expand All @@ -90,15 +90,15 @@ 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 {
log.Error(err)
}
}

// 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 {
Expand All @@ -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)
Expand All @@ -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)
Expand All @@ -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() {
Expand All @@ -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 {
Expand Down Expand Up @@ -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 {
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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))
Expand Down