mirror of
https://github.com/hugmouse/maddy-password-reset.git
synced 2025-04-19 13:00:26 +00:00
Compare commits
No commits in common. "733287e36bc28f0abc6a3d7f181edc28cc0ddba6" and "02a75b7d5388f5415dd0e9b61958bc25da66b5e8" have entirely different histories.
733287e36b
...
02a75b7d53
3 changed files with 112 additions and 374 deletions
396
main.go
396
main.go
|
@ -18,7 +18,6 @@ package main
|
||||||
import (
|
import (
|
||||||
cryptorand "crypto/rand"
|
cryptorand "crypto/rand"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/akyoto/cache"
|
"github.com/akyoto/cache"
|
||||||
"github.com/hugmouse/maddy-password-reset/templates"
|
"github.com/hugmouse/maddy-password-reset/templates"
|
||||||
|
@ -38,39 +37,37 @@ import (
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
// Configuration
|
|
||||||
// Make sure to set these constants before running the program
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
const (
|
const (
|
||||||
// MaddyPath is path to your Maddy credentials database
|
// MaddyPath is path to your Maddy credentials database
|
||||||
|
//
|
||||||
// FYI, Maddy's password database by default is "/var/lib/maddy/credentials.db"
|
// FYI, Maddy's password database by default is "/var/lib/maddy/credentials.db"
|
||||||
MaddyPath = "maddy.db" // MUST be set
|
MaddyPath = ""
|
||||||
|
|
||||||
// HostingURL is your domain name, including scheme and trailing slash,
|
// HostingURL is your domain name,
|
||||||
// for example: `http://localhost:1323/` or `https://example.com/`
|
// for example: `http://localhost:1323/`
|
||||||
HostingURL = "http://localhost:1323/" // MUST be set
|
HostingURL = ""
|
||||||
|
|
||||||
// SMTPMailUsername is your full mail address,
|
// SMTPMailUsername is your full mail address,
|
||||||
// for example: `robot@local.host`
|
// for example: `robot@local.host`
|
||||||
SMTPMailUsername = "" // MUST be set (unless DebugBypassMailSending is true)
|
SMTPMailUsername = ""
|
||||||
|
|
||||||
// SMTPMailPassword is your mailbox password
|
// SMTPMailPassword is your mailbox password
|
||||||
SMTPMailPassword = "" // MUST be set (unless DebugBypassMailSending is true)
|
SMTPMailPassword = ""
|
||||||
|
|
||||||
// SMTPMailHostname is your mail hostname,
|
// SMTPMailHostname is your mail hostname,
|
||||||
// for example: `mx1.local.host`
|
// for example: `mx1.local.host`
|
||||||
SMTPMailHostname = "" // MUST be set (unless DebugBypassMailSending is true)
|
SMTPMailHostname = ""
|
||||||
|
|
||||||
// MXServer is your mail `MX` record + `PORT`,
|
// MXServer is your mail `MX` record + `PORT`,
|
||||||
// for example: `mx1.local.host:587`
|
// for example: `mx1.local.host:587`
|
||||||
MXServer = "" // MUST be set (unless DebugBypassMailSending is true)
|
MXServer = ""
|
||||||
|
|
||||||
// EmailFrom is a EmailTemplate's "$FROM" section
|
// EmailFrom is a EmailTemplate's "$FROM" section
|
||||||
EmailFrom = "" // MUST be set (unless DebugBypassMailSending is true)
|
EmailFrom = ""
|
||||||
// EmailSubject is a EmailTemplate's "$SUBJECT" section
|
// EmailSubject is a EmailTemplate's "$SUBJECT" section
|
||||||
EmailSubject = "" // MUST be set (unless DebugBypassMailSending is true)
|
EmailSubject = ""
|
||||||
// EmailMessage is a EmailTemplate's "$MESSAGE" section
|
// EmailMessage is a EmailTemplate's "$MESSAGE" section
|
||||||
|
//
|
||||||
// Remember to provide a password reset link to a user ($RESET_LINK)
|
// Remember to provide a password reset link to a user ($RESET_LINK)
|
||||||
EmailMessage = "Here's your reset link: $RESET_LINK\r\n"
|
EmailMessage = "Here's your reset link: $RESET_LINK\r\n"
|
||||||
// EmailTemplate is your reset mail message
|
// EmailTemplate is your reset mail message
|
||||||
|
@ -87,102 +84,27 @@ const (
|
||||||
// HTTPServerPort is an HTTP server port
|
// HTTPServerPort is an HTTP server port
|
||||||
HTTPServerPort = 1323
|
HTTPServerPort = 1323
|
||||||
|
|
||||||
// DebugBypassMailSending - if true, skips SMTP checks and sending, logs reset link instead.
|
// DebugBypassMailSending if true, will not send any emails and will print reset link to the console
|
||||||
DebugBypassMailSending = false
|
DebugBypassMailSending = true
|
||||||
)
|
)
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
// Constants for random string generator
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
const (
|
const (
|
||||||
TokenAlphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
// TokenAlphabet is created for random string creation, see randomString() function
|
||||||
RandomStringLength = 10
|
TokenAlphabet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||||
)
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
// Log Message Constants
|
|
||||||
// ---------------------------------------------------------------
|
|
||||||
const (
|
|
||||||
// Configuration Validation
|
|
||||||
logMsgConfigValidationStart = "[Startup] Validating configuration..."
|
|
||||||
logMsgConfigMaddyPathEmpty = "[Config] MaddyPath constant cannot be empty."
|
|
||||||
logMsgConfigHostingURLEmpty = "[Config] HostingURL constant cannot be empty."
|
|
||||||
logMsgConfigHostingURLSlashFmt = "[Config] HostingURL must end with a trailing slash '/'. Current value: %s"
|
|
||||||
logMsgConfigSmtpCheckStart = "[Config] Checking SMTP and Email Template configuration (DebugBypassMailSending is false)"
|
|
||||||
logMsgConfigSmtpUsernameEmpty = "[Config] SMTPMailUsername constant cannot be empty when DebugBypassMailSending is false."
|
|
||||||
logMsgConfigSmtpPasswordEmpty = "[Config] SMTPMailPassword constant cannot be empty when DebugBypassMailSending is false."
|
|
||||||
logMsgConfigSmtpHostnameEmpty = "[Config] SMTPMailHostname constant cannot be empty when DebugBypassMailSending is false."
|
|
||||||
logMsgConfigMxServerEmpty = "[Config] MXServer constant cannot be empty when DebugBypassMailSending is false."
|
|
||||||
logMsgConfigEmailFromEmpty = "[Config] EmailFrom constant cannot be empty when DebugBypassMailSending is false."
|
|
||||||
logMsgConfigEmailSubjectEmpty = "[Config] EmailSubject constant cannot be empty when DebugBypassMailSending is false."
|
|
||||||
logMsgConfigEmailMsgResetLinkCheck = "[EmailMessage const] Checking your template for $RESET_LINK"
|
|
||||||
logMsgConfigEmailMsgResetLinkMissing = "[EmailMessage const] Your message template does not contain $RESET_LINK, so user can't reset his password!"
|
|
||||||
logMsgConfigEmailTplPlaceholdersCheck = "[EmailTemplate const] Checking your template placeholders ($TO, $FROM, $SUBJECT, $MESSAGE)"
|
|
||||||
logMsgConfigEmailTplPlaceholderMissingFmt = "[EmailTemplate const] Your template does not contain %s, make sure to add it."
|
|
||||||
logMsgConfigSmtpCheckSkipped = "[Config] DebugBypassMailSending is true. Skipping SMTP and Email Template configuration checks."
|
|
||||||
logMsgConfigCacheTimeInvalid = "[Config] CacheTime must be a positive duration."
|
|
||||||
logMsgConfigHttpPortInvalid = "[Config] HTTPServerPort must be a valid port number (1-65535)."
|
|
||||||
|
|
||||||
// SMTP
|
|
||||||
logMsgSmtpAuthSetup = "[SMTP] Configuring SMTP authentication."
|
|
||||||
logMsgSmtpAuthSkipped = "[SMTP] Debug mode enabled, skipping SMTP authentication setup."
|
|
||||||
logMsgSmtpSendFailedFmt = "[SMTP] Failed to send password reset email to %q: %v"
|
|
||||||
logMsgSmtpSendSuccessFmt = "[SMTP] Password reset email successfully sent to %q"
|
|
||||||
logMsgSmtpDebugSendSkippedFmt = "[SMTP] Debug mode enabled. Would send reset email to %q."
|
|
||||||
logMsgSmtpDebugResetLinkFmt = "[SMTP] Reset link for %q: %s"
|
|
||||||
|
|
||||||
// Sqlite
|
|
||||||
logMsgDbLoadingFmt = "[Sqlite] Loading Maddy's credentials database from: %s"
|
|
||||||
logMsgDbOpenFailedFmt = "[Sqlite] Failed to open database: %v"
|
|
||||||
logMsgDbClosing = "[Sqlite] Closing database connection."
|
|
||||||
logMsgDbCloseErrorFmt = "[Sqlite] Error closing database: %v"
|
|
||||||
logMsgDbPingFailedFmt = "[Sqlite] Failed to connect to database: %v"
|
|
||||||
logMsgDbPingSuccess = "[Sqlite] Database connection successful."
|
|
||||||
logMsgDbUserNotFoundFmt = "[Sqlite] User email %q not found in Maddy database. No reset link generated."
|
|
||||||
logMsgDbQueryErrorFmt = "[Sqlite] Error querying Maddy database for user %q: %v"
|
|
||||||
|
|
||||||
// Cache
|
|
||||||
logMsgCacheRegisterFmt = "[Cache] Registering cache for password resets with expiry: %v"
|
|
||||||
logMsgCacheResetPendingFmt = "[Cache] Password reset request for %q already pending, ignoring new request."
|
|
||||||
|
|
||||||
// Echo server
|
|
||||||
logMsgEchoInit = "[Echo] Initializing echo web server"
|
|
||||||
logMsgEchoTplRegister = "[Echo] Registering Go templates"
|
|
||||||
logMsgEchoStartFmt = "[Echo] Starting Echo web server on %s"
|
|
||||||
logMsgEchoStartFailedFmt = "[Echo] Failed to start server: %v"
|
|
||||||
|
|
||||||
// Handlers
|
|
||||||
logMsgHandlerPostResetInvalidEmailFmt = "[Handler POST /reset] Invalid email address provided '%s': %v"
|
|
||||||
logMsgHandlerGetKeyEmpty = "[Handler GET /reset/:key] Received empty key."
|
|
||||||
logMsgHandlerGetKeyInvalidFmt = "[Handler GET /reset/:key] Invalid or expired key provided: %s"
|
|
||||||
logMsgHandlerPostKeyEmpty = "[Handler POST /reset/:key] Received empty key."
|
|
||||||
logMsgHandlerPostKeyEmptyPassword = "[Handler POST /reset/:key] Received empty password."
|
|
||||||
logMsgHandlerPostKeyInvalidFmt = "[Handler POST /reset/:key] Invalid or expired key submitted: %s"
|
|
||||||
logMsgHandlerPostKeyCacheTypeFmt = "[Handler POST /reset/:key] Value retrieved from cache for key %s is not a string: %T"
|
|
||||||
|
|
||||||
// Reset Process
|
|
||||||
logMsgResetTokenGeneratedFmt = "[Reset] Generated reset token %s for user %s. Link: %s"
|
|
||||||
|
|
||||||
// Maddy Command Execution
|
|
||||||
logMsgMaddyExecAttemptFmt = "[Maddy] Attempting to reset password for user %s via maddy command."
|
|
||||||
logMsgMaddyExecFailedFmt = "[Maddy] Failed to execute Maddy password reset command for %s: %v. Output: %s"
|
|
||||||
logMsgMaddyExecSuccessFmt = "[Maddy] Successfully reset password for user %s. Output: %s"
|
|
||||||
|
|
||||||
// Other
|
|
||||||
logMsgRandNumGenFailedCriticalFmt = "CRITICAL: Failed to generate random number: %v"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func randomString(length int) string {
|
func randomString(length int) string {
|
||||||
l := big.NewInt(int64(len(TokenAlphabet)))
|
l := big.NewInt(int64(len(TokenAlphabet)))
|
||||||
res := new(strings.Builder)
|
res := new(strings.Builder)
|
||||||
res.Grow(length)
|
|
||||||
for i := 0; i < length; i++ {
|
for i := 0; i < length; i++ {
|
||||||
n, err := cryptorand.Int(cryptorand.Reader, l)
|
n, err := cryptorand.Int(cryptorand.Reader, l)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf(logMsgRandNumGenFailedCriticalFmt, err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
res.WriteByte(TokenAlphabet[n.Int64()])
|
res.WriteByte(TokenAlphabet[n.Int64()])
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.String()
|
return res.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -190,132 +112,80 @@ type Template struct {
|
||||||
templates *template.Template
|
templates *template.Template
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Template) Render(w io.Writer, name string, data interface{}, c echo.Context) error {
|
func (t *Template) Render(w io.Writer, name string, data interface{}, _ echo.Context) error {
|
||||||
return t.templates.ExecuteTemplate(w, name, data)
|
return t.templates.ExecuteTemplate(w, name, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func isValidEmailAddress(email string) error {
|
func isValidEmailAddress(email string) error {
|
||||||
if email == "" {
|
// Parse the email address using addressparser
|
||||||
return errors.New("email address cannot be empty")
|
mail, err := mail.ParseAddress(email)
|
||||||
}
|
|
||||||
|
|
||||||
addr, err := mail.ParseAddress(email)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid email format: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.Contains(addr.Address, "@") {
|
// Check if the parsed address is not nil and has a valid email format
|
||||||
return errors.New("invalid email format: missing @ symbol")
|
if mail == nil || mail.Address == "" {
|
||||||
|
log.Println("[AddressParser]: Invalid Email Address: %v")
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateConfiguration() {
|
|
||||||
if MaddyPath == "" {
|
|
||||||
log.Fatalln(logMsgConfigMaddyPathEmpty)
|
|
||||||
}
|
|
||||||
|
|
||||||
if HostingURL == "" {
|
|
||||||
log.Fatalln(logMsgConfigHostingURLEmpty)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.HasSuffix(HostingURL, "/") {
|
|
||||||
log.Fatalf(logMsgConfigHostingURLSlashFmt, HostingURL)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !DebugBypassMailSending {
|
|
||||||
log.Println(logMsgConfigSmtpCheckStart)
|
|
||||||
if SMTPMailUsername == "" {
|
|
||||||
log.Fatalln(logMsgConfigSmtpUsernameEmpty)
|
|
||||||
}
|
|
||||||
if SMTPMailPassword == "" {
|
|
||||||
log.Fatalln(logMsgConfigSmtpPasswordEmpty)
|
|
||||||
}
|
|
||||||
if SMTPMailHostname == "" {
|
|
||||||
log.Fatalln(logMsgConfigSmtpHostnameEmpty)
|
|
||||||
}
|
|
||||||
if MXServer == "" {
|
|
||||||
log.Fatalln(logMsgConfigMxServerEmpty)
|
|
||||||
}
|
|
||||||
if EmailFrom == "" {
|
|
||||||
log.Fatalln(logMsgConfigEmailFromEmpty)
|
|
||||||
}
|
|
||||||
if EmailSubject == "" {
|
|
||||||
log.Fatalln(logMsgConfigEmailSubjectEmpty)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println(logMsgConfigEmailMsgResetLinkCheck)
|
|
||||||
if !strings.Contains(EmailMessage, "$RESET_LINK") {
|
|
||||||
log.Fatalln(logMsgConfigEmailMsgResetLinkMissing)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println(logMsgConfigEmailTplPlaceholdersCheck)
|
|
||||||
requiredPlaceholders := []string{"$TO", "$FROM", "$SUBJECT", "$MESSAGE"}
|
|
||||||
for _, placeholder := range requiredPlaceholders {
|
|
||||||
if !strings.Contains(EmailTemplate, placeholder) {
|
|
||||||
log.Fatalf(logMsgConfigEmailTplPlaceholderMissingFmt, placeholder)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
log.Println(logMsgConfigSmtpCheckSkipped)
|
|
||||||
}
|
|
||||||
|
|
||||||
if CacheTime <= 0 {
|
|
||||||
log.Fatalln(logMsgConfigCacheTimeInvalid)
|
|
||||||
}
|
|
||||||
|
|
||||||
if HTTPServerPort <= 0 || HTTPServerPort > 65535 {
|
|
||||||
log.Fatalln(logMsgConfigHttpPortInvalid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
log.Println(logMsgConfigValidationStart)
|
|
||||||
validateConfiguration()
|
|
||||||
|
|
||||||
var auth smtp.Auth
|
var auth smtp.Auth
|
||||||
if !DebugBypassMailSending {
|
if !DebugBypassMailSending {
|
||||||
log.Println(logMsgSmtpAuthSetup)
|
log.Println("[EmailMessage const] Checking your template")
|
||||||
|
if !strings.Contains(EmailMessage, "$RESET_LINK") {
|
||||||
|
log.Fatalln("[EmailMessage const] Your message template does not contain $RESET_LINK, so user can't reset his password!")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("[EmailTemplate const] Checking your template")
|
||||||
|
if !strings.Contains(EmailTemplate, "$TO") {
|
||||||
|
log.Fatalln("[EmailTemplate const] Your template does not contain $TO, make sure to add it.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(EmailTemplate, "$FROM") {
|
||||||
|
log.Fatalln("[EmailTemplate const] Your template does not contain $FROM, make sure to add it.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(EmailTemplate, "$SUBJECT") {
|
||||||
|
log.Fatalln("[EmailTemplate const] Your template does not contain $SUBJECT, make sure to add it, so user can see a message preview.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(EmailTemplate, "$MESSAGE") {
|
||||||
|
log.Fatalln("[EmailTemplate const] Your template does not contain $MESSAGE, make sure to add it.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up authentication information.
|
||||||
auth = smtp.PlainAuth("", SMTPMailUsername, SMTPMailPassword, SMTPMailHostname)
|
auth = smtp.PlainAuth("", SMTPMailUsername, SMTPMailPassword, SMTPMailHostname)
|
||||||
} else {
|
} else {
|
||||||
log.Println(logMsgSmtpAuthSkipped)
|
log.Println("[SMTP] Debug mode enabled, not checking email template")
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf(logMsgDbLoadingFmt, MaddyPath)
|
log.Println("[Sqlite] Loading Maddy's credentials database")
|
||||||
db, err := sql.Open("sqlite", MaddyPath)
|
db, err := sql.Open("sqlite", MaddyPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf(logMsgDbOpenFailedFmt, err)
|
log.Fatalln(err)
|
||||||
}
|
}
|
||||||
defer func() {
|
|
||||||
log.Println(logMsgDbClosing)
|
|
||||||
if err := db.Close(); err != nil {
|
|
||||||
log.Printf(logMsgDbCloseErrorFmt, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if err := db.Ping(); err != nil {
|
log.Println("[Cache] Registering cache for password resets")
|
||||||
log.Fatalf(logMsgDbPingFailedFmt, err)
|
|
||||||
}
|
|
||||||
log.Println(logMsgDbPingSuccess)
|
|
||||||
|
|
||||||
log.Printf(logMsgCacheRegisterFmt, CacheTime)
|
|
||||||
passwordResetCache := cache.New(CacheTime)
|
passwordResetCache := cache.New(CacheTime)
|
||||||
|
|
||||||
log.Println(logMsgEchoInit)
|
log.Println("[Echo] Initializing echo web server")
|
||||||
e := echo.New()
|
e := echo.New()
|
||||||
e.HideBanner = true
|
e.HideBanner = true
|
||||||
e.Use(middleware.LoggerWithConfig(
|
e.Use(middleware.LoggerWithConfig(
|
||||||
middleware.LoggerConfig{
|
middleware.LoggerConfig{
|
||||||
Format: `${time_custom} [Echo] ${latency_human} ${method} ${uri} - Status=${status} Error="${error}" RemoteIP=${remote_ip} UserAgent="${user_agent}"` + "\n",
|
Format: `${time_custom} [Echo] ${latency_human} ${method} ${uri} - Error = ${error} - ${remote_ip} "${user_agent}"` + "\n",
|
||||||
CustomTimeFormat: "2006/01/02 15:04:05",
|
CustomTimeFormat: "2006/01/02 15:04:05",
|
||||||
}))
|
}))
|
||||||
e.Use(middleware.Recover())
|
e.Use(middleware.Recover())
|
||||||
|
|
||||||
log.Println(logMsgEchoTplRegister)
|
log.Println("[Echo] Registering Go templates")
|
||||||
t := template.Must(template.ParseFS(templates.Templates, "*.gohtml"))
|
t := template.Must(template.ParseFS(templates.Templates, "*.gohtml"))
|
||||||
e.Renderer = &Template{
|
e.Renderer = &Template{
|
||||||
templates: t,
|
t,
|
||||||
}
|
}
|
||||||
|
|
||||||
e.GET("/reset", func(c echo.Context) error {
|
e.GET("/reset", func(c echo.Context) error {
|
||||||
|
@ -323,155 +193,89 @@ func main() {
|
||||||
})
|
})
|
||||||
|
|
||||||
e.POST("/reset", func(c echo.Context) error {
|
e.POST("/reset", func(c echo.Context) error {
|
||||||
email := c.FormValue("email")
|
mail := c.FormValue("email")
|
||||||
if err := isValidEmailAddress(email); err != nil {
|
err = isValidEmailAddress(mail)
|
||||||
log.Printf(logMsgHandlerPostResetInvalidEmailFmt, email, err)
|
if err != nil {
|
||||||
return c.Render(http.StatusBadRequest, "reset.gohtml", map[string]any{
|
log.Println("[AddressParser]: Invalid mail address: ", err)
|
||||||
"Error": "Invalid email address format provided.",
|
return echo.NewHTTPError(http.StatusBadRequest, fmt.Sprintf("Invalid mail address: %v", err))
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
go func() {
|
||||||
go func(userEmail string) {
|
// Check if there is already a password reset
|
||||||
|
_, exists := passwordResetCache.Get(mail)
|
||||||
// Check if there is already a pending password reset for this email
|
|
||||||
_, exists := passwordResetCache.Get(userEmail)
|
|
||||||
if exists {
|
if exists {
|
||||||
log.Printf(logMsgCacheResetPendingFmt, userEmail)
|
log.Printf("[Cache] Mail %q already exists in cache, ignoring\n", mail)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user exists in Maddy DB
|
// Check if it's exists in Maddy db
|
||||||
var dummy int
|
// It will return an error is there is no user found
|
||||||
dbErr := db.QueryRow("SELECT 1 FROM passwords WHERE key = ?", userEmail).Scan(&dummy)
|
var password string
|
||||||
|
err = db.QueryRow("SELECT value FROM passwords WHERE key = ?", mail).Scan(&password)
|
||||||
if dbErr != nil {
|
if err != nil {
|
||||||
if errors.Is(dbErr, sql.ErrNoRows) {
|
log.Println("[Sqlite] An error occurred while trying to get password from Maddy database:", err)
|
||||||
log.Printf(logMsgDbUserNotFoundFmt, userEmail)
|
|
||||||
} else {
|
|
||||||
log.Printf(logMsgDbQueryErrorFmt, userEmail, dbErr)
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
token := randomString(RandomStringLength)
|
// Generating an unique key
|
||||||
passwordResetCache.Set(token, userEmail, CacheTime)
|
random := randomString(10)
|
||||||
passwordResetCache.Set(userEmail, token, CacheTime)
|
passwordResetCache.Set(random, mail, CacheTime)
|
||||||
|
|
||||||
resetLink := HostingURL + "reset/" + token
|
// Connect to the server, authenticate, set the sender and recipient,
|
||||||
log.Printf(logMsgResetTokenGeneratedFmt, token, userEmail, resetLink)
|
// and send the email all in one step.
|
||||||
|
to := []string{mail}
|
||||||
|
|
||||||
if !DebugBypassMailSending {
|
if !DebugBypassMailSending {
|
||||||
msg := strings.ReplaceAll(EmailTemplate, "$TO", userEmail)
|
msg := strings.ReplaceAll(EmailTemplate, "$TO", mail)
|
||||||
msg = strings.ReplaceAll(msg, "$FROM", EmailFrom)
|
msg = strings.ReplaceAll(msg, "$FROM", EmailFrom)
|
||||||
msg = strings.ReplaceAll(msg, "$SUBJECT", EmailSubject)
|
msg = strings.ReplaceAll(msg, "$SUBJECT", EmailSubject)
|
||||||
|
msg = strings.ReplaceAll(msg, "$MESSAGE", EmailMessage)
|
||||||
|
msg = strings.ReplaceAll(msg, "$RESET_LINK", HostingURL+"reset/"+random)
|
||||||
|
|
||||||
messageBody := strings.ReplaceAll(EmailMessage, "$RESET_LINK", resetLink)
|
err := smtp.SendMail(MXServer, auth, SMTPMailUsername, to, []byte(msg))
|
||||||
msg = strings.ReplaceAll(msg, "$MESSAGE", messageBody)
|
if err != nil {
|
||||||
|
log.Println("[SMTP] Failed to send mail - ", err)
|
||||||
to := []string{userEmail}
|
|
||||||
smtpErr := smtp.SendMail(MXServer, auth, SMTPMailUsername, to, []byte(msg))
|
|
||||||
if smtpErr != nil {
|
|
||||||
log.Printf(logMsgSmtpSendFailedFmt, userEmail, smtpErr)
|
|
||||||
// Clean up cache if sending failed
|
|
||||||
passwordResetCache.Delete(token)
|
|
||||||
passwordResetCache.Delete(userEmail)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Printf(logMsgSmtpSendSuccessFmt, userEmail)
|
|
||||||
} else {
|
} else {
|
||||||
log.Printf(logMsgSmtpDebugSendSkippedFmt, userEmail)
|
log.Println("[SMTP] Debug mode enabled, not sending email")
|
||||||
log.Printf(logMsgSmtpDebugResetLinkFmt, userEmail, resetLink)
|
log.Println("[SMTP] Reset link:", HostingURL+"reset/"+random)
|
||||||
}
|
}
|
||||||
}(email)
|
}()
|
||||||
|
|
||||||
// Always return success to the user to prevent email enumeration
|
|
||||||
return c.Render(http.StatusOK, "reset.gohtml", map[string]any{
|
return c.Render(http.StatusOK, "reset.gohtml", map[string]any{
|
||||||
"Sent": true, // Indicates request received, not necessarily email sent successfully
|
"Sent": true,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
e.GET("/reset/:key", func(c echo.Context) error {
|
e.GET("/reset/:key", func(c echo.Context) error {
|
||||||
key := c.Param("key")
|
key := c.Param("key")
|
||||||
if key == "" {
|
|
||||||
log.Println(logMsgHandlerGetKeyEmpty)
|
|
||||||
return c.Redirect(http.StatusTemporaryRedirect, "/reset")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, exists := passwordResetCache.Get(key)
|
_, exists := passwordResetCache.Get(key)
|
||||||
if !exists {
|
if !exists {
|
||||||
log.Printf(logMsgHandlerGetKeyInvalidFmt, key)
|
return c.Redirect(http.StatusTemporaryRedirect, "/reset")
|
||||||
return c.Render(http.StatusNotFound, "reset.gohtml", map[string]any{
|
|
||||||
"Error": "This password reset link is invalid or has expired.",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.Render(http.StatusOK, "reset.gohtml", map[string]any{
|
return c.Render(http.StatusOK, "reset.gohtml", map[string]any{
|
||||||
"UniqueLinkTriggered": true,
|
"UniqueLinkTriggered": true,
|
||||||
"Key": key,
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
e.POST("/reset/:key", func(c echo.Context) error {
|
e.POST("/reset/:key", func(c echo.Context) error {
|
||||||
key := c.Param("key")
|
key := c.Param("key")
|
||||||
password := c.FormValue("password")
|
password := c.FormValue("password")
|
||||||
|
mail, exists := passwordResetCache.Get(key)
|
||||||
if key == "" {
|
if exists {
|
||||||
log.Println(logMsgHandlerPostKeyEmpty)
|
|
||||||
return c.Render(http.StatusBadRequest, "reset.gohtml", map[string]any{
|
|
||||||
"Error": "Reset key is missing.",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if password == "" {
|
|
||||||
log.Println(logMsgHandlerPostKeyEmptyPassword)
|
|
||||||
return c.Render(http.StatusBadRequest, "reset.gohtml", map[string]any{
|
|
||||||
"UniqueLinkTriggered": true,
|
|
||||||
"Key": key,
|
|
||||||
"Error": "Password cannot be empty.",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
emailVal, exists := passwordResetCache.Get(key)
|
|
||||||
if !exists {
|
|
||||||
log.Printf(logMsgHandlerPostKeyInvalidFmt, key)
|
|
||||||
return c.Render(http.StatusNotFound, "reset.gohtml", map[string]any{
|
|
||||||
"Error": "This password reset link is invalid or has expired. Please request a new one.",
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
email, ok := emailVal.(string)
|
|
||||||
if !ok {
|
|
||||||
log.Printf(logMsgHandlerPostKeyCacheTypeFmt, key, emailVal)
|
|
||||||
passwordResetCache.Delete(key)
|
passwordResetCache.Delete(key)
|
||||||
passwordResetCache.Delete(email)
|
|
||||||
return c.Render(http.StatusInternalServerError, "reset.gohtml", map[string]any{
|
|
||||||
"Error": "An internal error occurred. Please try again.",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invalidate stuff
|
maddyExecCommand := exec.Command("maddy", "creds", "password", "-p", password, mail.(string))
|
||||||
passwordResetCache.Delete(key)
|
err = maddyExecCommand.Run()
|
||||||
passwordResetCache.Delete(email)
|
if err != nil {
|
||||||
|
log.Println("[maddyExecCommand] Failed to execute Maddy's password reset command - ", err)
|
||||||
log.Printf(logMsgMaddyExecAttemptFmt, email)
|
return err
|
||||||
maddyExecCommand := exec.Command("maddy", "creds", "password", "-p", password, email)
|
|
||||||
output, execErr := maddyExecCommand.CombinedOutput()
|
|
||||||
|
|
||||||
if execErr != nil {
|
|
||||||
log.Printf(logMsgMaddyExecFailedFmt, email, execErr, string(output))
|
|
||||||
return c.Render(http.StatusInternalServerError, "reset.gohtml", map[string]any{
|
|
||||||
"Error": "Failed to update password due to a server error.",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf(logMsgMaddyExecSuccessFmt, email, string(output))
|
return c.String(http.StatusOK, "All good! Your password is now changed.")
|
||||||
return c.Render(http.StatusOK, "reset.gohtml", map[string]any{
|
|
||||||
"Success": "Password successfully changed! You can now log in with your new password.",
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
serverAddr := ":" + strconv.Itoa(HTTPServerPort)
|
log.Println("[echo] Starting Echo web server")
|
||||||
log.Printf(logMsgEchoStartFmt, serverAddr) // Use Printf for format string
|
e.Logger.Fatal(e.Start(":" + strconv.Itoa(HTTPServerPort)))
|
||||||
if err := e.Start(serverAddr); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
|
||||||
log.Fatalf(logMsgEchoStartFailedFmt, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,62 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
|
||||||
<title>Password Reset</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
padding: 1rem;
|
|
||||||
font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
margin-top: 1rem;
|
|
||||||
padding: 1rem;
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid black;
|
|
||||||
color: black;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
body {
|
|
||||||
background-color: #222;
|
|
||||||
color: white;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
border: 1px solid #ffffff;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Password Reset</h1>
|
|
||||||
{{ if .Error }}
|
|
||||||
{{ .Error }}
|
|
||||||
{{ else }}
|
|
||||||
{{ if .UniqueLinkTriggered }}
|
|
||||||
<p>Enter your new password below.</p>
|
|
||||||
<form action="" method="post">
|
|
||||||
<label for="password">Your new password</label>
|
|
||||||
<input type="password" name="password" id="password" placeholder="Enter your new password">
|
|
||||||
</form>
|
|
||||||
{{ else }}
|
|
||||||
{{ if .Sent }}
|
|
||||||
<p>A password reset email has been sent if that address exists.</p>
|
|
||||||
{{ else }}
|
|
||||||
<form action="/reset" method="post">
|
|
||||||
<label for="email">Email address</label>
|
|
||||||
<input type="email" name="email" id="email" placeholder="Enter your email address">
|
|
||||||
</form>
|
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -38,24 +38,20 @@
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Сброс пароля</h1>
|
<h1>Сброс пароля</h1>
|
||||||
{{ if .Error }}
|
{{ if .UniqueLinkTriggered }}
|
||||||
{{ .Error }}
|
<p>Напишите здесь ваш новый пароль</p>
|
||||||
|
<form action="" method="post">
|
||||||
|
<label for="password">Ваш новый пароль</label>
|
||||||
|
<input type="password" name="password" id="password" placeholder="Введите свой новый пароль">
|
||||||
|
</form>
|
||||||
{{ else }}
|
{{ else }}
|
||||||
{{ if .UniqueLinkTriggered }}
|
{{ if .Sent }}
|
||||||
<p>Напишите здесь ваш новый пароль</p>
|
<p>Сообщение о сбросе пароля было отправлено, если такой адрес существует.</p>
|
||||||
<form action="" method="post">
|
|
||||||
<label for="password">Ваш новый пароль</label>
|
|
||||||
<input type="password" name="password" id="password" placeholder="Введите свой новый пароль">
|
|
||||||
</form>
|
|
||||||
{{ else }}
|
{{ else }}
|
||||||
{{ if .Sent }}
|
<form action="/reset" method="post">
|
||||||
<p>Сообщение о сбросе пароля было отправлено, если такой адрес существует.</p>
|
<label for="email">Email адрес</label>
|
||||||
{{ else }}
|
<input type="email" name="email" id="email" placeholder="Введите свой email адрес">
|
||||||
<form action="/reset" method="post">
|
</form>
|
||||||
<label for="email">Email адрес</label>
|
|
||||||
<input type="email" name="email" id="email" placeholder="Введите свой email адрес">
|
|
||||||
</form>
|
|
||||||
{{ end }}
|
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
</body>
|
</body>
|
||||||
|
|
Loading…
Add table
Reference in a new issue