Skip to content
Merged
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
6 changes: 3 additions & 3 deletions api/internal/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,11 @@ const (
TokenTypePasswordReset string = "password_reset" //nolint:gosec
TokenTypeReauthEmailChange string = "reauth_email_change" //nolint:gosec
TokenTypeReauthAccountDeletion string = "reauth_account_deletion" //nolint:gosec
passwordResetTokenLength int = 128
passwordResetTokenLength int = 48
passwordResetTokenExpiration time.Duration = time.Hour
reauthEmailChangeTokenLength int = 128
reauthEmailChangeTokenLength int = 48
reauthEmailChangeExpiration time.Duration = time.Minute * 5
reauthAccountDeletionTokenLength int = 128
reauthAccountDeletionTokenLength int = 48
reauthAccountDeletionExpiration time.Duration = time.Minute * 5
)

Expand Down
39 changes: 39 additions & 0 deletions api/internal/email/content/content.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package content

import "fmt"

const SupportEmail = "moddns@ivpn.net"

// EmailContent holds subject, plain text, and HTML for an email.
type EmailContent struct {
Subject string
Plain string
Html string
}

// WelcomeContent returns the welcome email content.
func WelcomeContent(homeURL, verifyURL string) EmailContent {
return EmailContent{
Subject: "Welcome to modDNS",
Plain: fmt.Sprintf("Hello,\n\nWelcome to modDNS. Get started with using the service here: %s \n\nWarning: your email is not verified. Account recovery and critical service notification emails are disabled for unverified addresses. Follow this link to verify your email in modDNS settings: %s\n\nSent by modDNS", homeURL, verifyURL),
Html: fmt.Sprintf("<p>Hello,</p><p>Welcome to modDNS. Get started with using the service here: <a href=\"%s\">%s</a></p><p><strong>Warning:</strong> your email is not verified. Account recovery and critical service notification emails are disabled for unverified addresses. Follow this link to verify your email in modDNS settings: <a href=\"%s\">%s</a></p><p>Sent by modDNS</p>", homeURL, homeURL, verifyURL, verifyURL),
}
}

// PasswordResetContent returns the password reset email content.
func PasswordResetContent(resetLink string) EmailContent {
return EmailContent{
Subject: "Reset your modDNS password",
Plain: fmt.Sprintf("Hello,\n\nYou have requested a password reset for your modDNS account.\n\nFollow this link to reset your password: %s\n\nThe URL is live for 60 minutes after generation.\n\nIf you did not request the password reset, please ignore this message or contact support at %s.\n\nRegards,\nmodDNS team", resetLink, SupportEmail),
Html: fmt.Sprintf("<p>Hello,</p><p>You have requested a password reset for your modDNS account.</p><p>Follow this link to reset your password: <a href=\"%s\">%s</a></p><p>The URL is live for 60 minutes after generation.</p><p>If you did not request the password reset, please ignore this message or contact support at <a href=\"mailto:%s\">%s</a>.</p><p>Regards,<br>modDNS team</p>", resetLink, resetLink, SupportEmail, SupportEmail),
}
}

// EmailVerificationOTPContent returns the email verification OTP content.
func EmailVerificationOTPContent(otp string) EmailContent {
return EmailContent{
Subject: "modDNS Email address verification",
Plain: fmt.Sprintf("Hello,\n\nHere is a one-time code to verify your modDNS registered email address: %s \n\nIt expires in 15 minutes.\n\nNote: Unverified recipients will not receive account recovery emails.\n\nSent by modDNS", otp),
Html: fmt.Sprintf("<p>Hello,</p><p>Here is a one-time code to verify your modDNS registered email address: <strong>%s</strong></p><p>It expires in 15 minutes.</p><p><em>Note: Unverified recipients will not receive account recovery emails.</em></p><p>Sent by modDNS</p>", otp),
}
}
40 changes: 12 additions & 28 deletions api/internal/email/mailpit/mailpit.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"io"
"net/http"

"github.com/ivpn/dns/api/internal/email/content"
"github.com/rs/zerolog/log"
)

Expand Down Expand Up @@ -55,53 +56,36 @@ type mailpitSendRequest struct {

// SendWelcomeEmail sends a welcome email to the user using Mailpit
func (m *Mailpit) SendWelcomeEmail(ctx context.Context, sendTo, _ string) error {
subject := "Welcome to modDNS"
verifyURL := fmt.Sprintf("%s/account-preferences", m.serverName)
homeURL := fmt.Sprintf("%s/home", m.serverName)
body := fmt.Sprintf("Hello,\n\nWelcome to modDNS. Get started with using the service here: %s \n\nWarning: your email is not verified. Account recovery and critical service notification emails are disabled for unverified addresses. Follow this link to verify your email in modDNS settings: %s\n\nSent by modDNS", homeURL, verifyURL)

c := content.WelcomeContent(fmt.Sprintf("%s/home", m.serverName), fmt.Sprintf("%s/account-preferences", m.serverName))
reqBody := mailpitSendRequest{
From: Email{Email: "info@moddns.net", Name: "modDNS"},
To: []Email{{Email: sendTo, Name: "User"}},
Subject: subject,
Text: body,
Subject: c.Subject,
Text: c.Plain,
}
return m.sendEmail(ctx, sendTo, reqBody)
}

// SendPasswordResetEmail sends a password reset email to the user using Mailpit
func (m *Mailpit) SendPasswordResetEmail(ctx context.Context, sendTo, passwordResetToken string) error {
passResetLink := fmt.Sprintf("%s/reset-password/%s", m.serverName, passwordResetToken)
subject := "modDNS Password Reset"
body := fmt.Sprintf("You requested a password reset.\n\nReset your password using the following link:\n%s", passResetLink)

c := content.PasswordResetContent(fmt.Sprintf("%s/reset-password/%s", m.serverName, passwordResetToken))
reqBody := mailpitSendRequest{
From: Email{
Email: "info@moddns.net",
Name: "modDNS",
},
To: []Email{
{
Email: sendTo,
Name: "User",
},
},
Subject: subject,
Text: body,
From: Email{Email: "info@moddns.net", Name: "modDNS"},
To: []Email{{Email: sendTo, Name: "User"}},
Subject: c.Subject,
Text: c.Plain,
}

return m.sendEmail(ctx, sendTo, reqBody)
}

// SendEmailVerificationOTP sends the OTP code to the user.
func (m *Mailpit) SendEmailVerificationOTP(ctx context.Context, sendTo, otp string) error {
subject := "modDNS Email address verification"
body := fmt.Sprintf("Hello,\n\nHere is a one-time code to verify your modDNS registered email address: %s \n\nIt expires in 15 minutes.\n\nNote: Unverified recipients will not receive account recovery emails.\n\nSent by modDNS", otp)
c := content.EmailVerificationOTPContent(otp)
reqBody := mailpitSendRequest{
From: Email{Email: "info@moddns.net", Name: "modDNS"},
To: []Email{{Email: sendTo, Name: "User"}},
Subject: subject,
Text: body,
Subject: c.Subject,
Text: c.Plain,
}
return m.sendEmail(ctx, sendTo, reqBody)
}
Expand Down
65 changes: 22 additions & 43 deletions api/internal/email/mailtrap/mailtrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,13 @@ import (
"net/http"

emailverifier "github.com/AfterShip/email-verifier"
"github.com/ivpn/dns/api/internal/email/content"
"github.com/rs/zerolog/log"
)

const (
WelcomeEmail = "welcome_email"
WelcomeEmailUUID = "3a3f383b-e72c-4e79-8aaa-12469dd9d34f"
PasswordReset = "password_reset"
PasswordResetUUID = "1541b133-9896-4ada-b857-d8fe5962ae09"
WelcomeEmail = "welcome_email"
PasswordReset = "password_reset"
)

var (
Expand All @@ -30,22 +29,16 @@ type Mailtrap struct {
httpClient *http.Client
serverName string
inboxId string
templates map[string]string
authToken string
verifier *emailverifier.Verifier
sendEndpoint string
}

// NewMailtrap creates a new Mailtrap instance
func NewMailtrap(serverName, inboxId, authToken string) *Mailtrap {
emailTemplates := map[string]string{
WelcomeEmail: WelcomeEmailUUID,
PasswordReset: PasswordResetUUID,
}
verifier := emailverifier.NewVerifier().EnableDomainSuggest()
sendEndpoint := fmt.Sprintf("https://sandbox.api.mailtrap.io/api/send/%s", inboxId)
return &Mailtrap{
templates: emailTemplates,
serverName: serverName,
inboxId: inboxId,
authToken: authToken,
Expand All @@ -56,20 +49,15 @@ func NewMailtrap(serverName, inboxId, authToken string) *Mailtrap {
}

// SendWelcomeEmail sends a welcome email to the user
// confirmation link removed; instruct in-app verification
func (m *Mailtrap) SendWelcomeEmail(ctx context.Context, sendTo, _ string) error {
homeURL := fmt.Sprintf("%s/home", m.serverName)
verifyURL := fmt.Sprintf("%s/account-preferences", m.serverName)
subject := "Welcome to modDNS"
text := fmt.Sprintf("Hello,\n\nWelcome to modDNS. Get started with using the service here: %s \n\nWarning: your email is not verified. Account recovery and critical service notification emails are disabled for unverified addresses. Follow this link to verify your email in modDNS settings: %s\n\nSent by modDNS", homeURL, verifyURL)
c := content.WelcomeContent(fmt.Sprintf("%s/home", m.serverName), fmt.Sprintf("%s/account-preferences", m.serverName))
req := SendEmailRequest{
From: From{Email: "moddns@demomailtrap.com", Name: "modDNS"},
To: []To{{Email: sendTo}},
Subject: subject,
Text: text,
Category: WelcomeEmail,
TemplateUUID: "", // send raw text instead of template
TemplateVariables: map[string]string{},
From: From{Email: "moddns@demomailtrap.com", Name: "modDNS"},
To: []To{{Email: sendTo}},
Subject: c.Subject,
Text: c.Plain,
Html: c.Html,
Category: WelcomeEmail,
}
if err := m.sendEmail(ctx, sendTo, req); err != nil {
return err
Expand All @@ -80,40 +68,31 @@ func (m *Mailtrap) SendWelcomeEmail(ctx context.Context, sendTo, _ string) error

// SendPasswordResetEmail sends a password reset email to the user
func (m *Mailtrap) SendPasswordResetEmail(ctx context.Context, sendTo, passwordResetToken string) error {
passResetLink := fmt.Sprintf("%s/reset-password/%s", m.serverName, passwordResetToken)
c := content.PasswordResetContent(fmt.Sprintf("%s/reset-password/%s", m.serverName, passwordResetToken))
req := SendEmailRequest{
From: From{
Email: "moddns@demomailtrap.com",
Name: "modDNS Team",
},
To: []To{
{
Email: sendTo,
},
},
TemplateUUID: m.templates[PasswordReset],
TemplateVariables: map[string]string{
"pass_reset_link": passResetLink,
},
From: From{Email: "moddns@demomailtrap.com", Name: "modDNS Team"},
To: []To{{Email: sendTo}},
Subject: c.Subject,
Text: c.Plain,
Html: c.Html,
Category: PasswordReset,
}

if err := m.sendEmail(ctx, sendTo, req); err != nil {
return err
}

log.Info().Str("email", sendTo).Msg("Password reset email sent successfully")
return nil
}

// SendEmailVerificationOTP sends the verification code email using a basic template (reuse password reset template or create new if needed)
// SendEmailVerificationOTP sends a 6-digit OTP code for email verification.
func (m *Mailtrap) SendEmailVerificationOTP(ctx context.Context, sendTo, otp string) error {
subject := "modDNS Email address verification"
body := fmt.Sprintf("Hello,\n\nHere is a one-time code to verify your modDNS registered email address: %s \n\nIt expires in 15 minutes.\n\nNote: Unverified recipients will not receive account recovery emails.\n\nSent by modDNS", otp)
c := content.EmailVerificationOTPContent(otp)
req := SendEmailRequest{
From: From{Email: "moddns@demomailtrap.com", Name: "modDNS Team"},
To: []To{{Email: sendTo}},
Subject: subject,
Text: body,
Subject: c.Subject,
Text: c.Plain,
Html: c.Html,
}
if err := m.sendEmail(ctx, sendTo, req); err != nil {
return err
Expand Down
13 changes: 6 additions & 7 deletions api/internal/email/mailtrap/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,12 @@ type To struct {

// SendEmailRequest represents the payload necessary to send an email in Mailtrap service
type SendEmailRequest struct {
From From
To []To `json:"to"`
Subject string `json:"subject"`
Text string `json:"text"`
Category string `json:"category"`
TemplateUUID string `json:"template_uuid"`
TemplateVariables map[string]string `json:"template_variables"`
From From
To []To `json:"to"`
Subject string `json:"subject"`
Text string `json:"text"`
Html string `json:"html,omitempty"`
Category string `json:"category"`
}

// SendEmailResponse represents the response from the Mailtrap email service
Expand Down
22 changes: 7 additions & 15 deletions api/internal/email/sendgrid/sendgrid.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"regexp"

"github.com/ivpn/dns/api/internal/email/content"
"github.com/rs/zerolog/log"
"github.com/sendgrid/rest"
sg "github.com/sendgrid/sendgrid-go"
Expand Down Expand Up @@ -57,29 +58,20 @@ func New(serverName, apiKey string, opts ...Option) *Mailer {

// SendWelcomeEmail sends welcome email with template dynamic data
func (m *Mailer) SendWelcomeEmail(ctx context.Context, sendTo, _ string) error {
subject := "Welcome to modDNS"
verifyURL := fmt.Sprintf("%s/account-preferences", m.serverName)
homeURL := fmt.Sprintf("%s/home", m.serverName)
plain := fmt.Sprintf("Hello,\n\nWelcome to modDNS. Get started with using the service here: %s \n\nWarning: your email is not verified. Account recovery and critical service notification emails are disabled of unverified addresses. Follow this link to verify your email in modDNS settings: %s\n\nSent by modDNS", homeURL, verifyURL)
html := fmt.Sprintf("<p>Hello,</p><p>Welcome to modDNS. Get started with using the service here: <a href=\"%s\">%s</a></p><p><strong>Warning:</strong> your email is not verified. Account recovery and critical service notification emails are disabled for unverified addresses. Follow this link to verify your email in modDNS settings: <a href=\"%s\">%s</a></p><p>Sent by modDNS</p>", homeURL, homeURL, verifyURL, verifyURL)
return m.sendBasic(ctx, sendTo, subject, plain, html)
c := content.WelcomeContent(fmt.Sprintf("%s/home", m.serverName), fmt.Sprintf("%s/account-preferences", m.serverName))
return m.sendBasic(ctx, sendTo, c.Subject, c.Plain, c.Html)
}

// SendEmailVerificationOTP sends a 6-digit OTP code for email verification.
func (m *Mailer) SendEmailVerificationOTP(ctx context.Context, sendTo, otp string) error {
subject := "modDNS Email address verification"
plain := fmt.Sprintf("Hello,\n\nHere is a one-time code to verify your modDNS registered email address: %s \n\nIt expires in 15 minutes.\n\nNote: Unverified recipients will not receive account recovery emails.\n\nSent by modDNS", otp)
html := fmt.Sprintf("<p>Hello,</p><p>Here is a one-time code to verify your modDNS registered email address: <strong>%s</strong></p><p>It expires in 15 minutes.</p><p><em>Note: Unverified recipients will not receive account recovery emails.</em></p><p>Sent by modDNS</p>", otp)
return m.sendBasic(ctx, sendTo, subject, plain, html)
c := content.EmailVerificationOTPContent(otp)
return m.sendBasic(ctx, sendTo, c.Subject, c.Plain, c.Html)
}

// SendPasswordResetEmail sends password reset email with template dynamic data
func (m *Mailer) SendPasswordResetEmail(ctx context.Context, sendTo, passwordResetToken string) error {
resetLink := fmt.Sprintf("%s/reset-password/%s", m.serverName, passwordResetToken)
subject := "Reset your modDNS password"
plain := fmt.Sprintf("Reset your password by visiting: %s\n\nSent by modDNS", resetLink)
html := fmt.Sprintf("<p>Reset your password by visiting: <a href=\"%s\">%s</a></p><p>Sent by modDNS</p>", resetLink, resetLink)
return m.sendBasic(ctx, sendTo, subject, plain, html)
c := content.PasswordResetContent(fmt.Sprintf("%s/reset-password/%s", m.serverName, passwordResetToken))
return m.sendBasic(ctx, sendTo, c.Subject, c.Plain, c.Html)
}

// Verify performs basic syntax validation. Extend with more advanced service if needed.
Expand Down
Loading