diff --git a/config/config.go b/config/config.go index 5ef83b1..40d7cbb 100644 --- a/config/config.go +++ b/config/config.go @@ -59,14 +59,16 @@ type AmConfig struct { Dsn string `yaml:"dsn"` } `yaml:"database"` Email struct { - Host string `yaml:"host"` - Port int `yaml:"port"` - Tls string `yaml:"tls"` - AuthType string `yaml:"authType"` - User string `yaml:"user"` - Password string `yaml:"password"` - Signature string `yaml:"signature"` - Disclaimer string `yaml:"disclaimer"` + Host string `yaml:"host"` + Port int `yaml:"port"` + Tls string `yaml:"tls"` + AuthType string `yaml:"authType"` + User string `yaml:"user"` + Password string `yaml:"password"` + MailFromAddr string `yaml:"mailFromAddr"` + MailFromName string `yaml:"mailFromName"` + Signature string `yaml:"signature"` + Disclaimer string `yaml:"disclaimer"` } `yaml:"email"` Rendering struct { TemplateDir string `yaml:"templatedir"` @@ -130,6 +132,8 @@ func overlayConfig(dest *AmConfig, loaded *AmConfig, defaults *AmConfig) { dest.Email.AuthType = overlayString(loaded.Email.AuthType, defaults.Email.AuthType) dest.Email.User = overlayString(loaded.Email.User, defaults.Email.User) dest.Email.Password = overlayString(loaded.Email.Password, defaults.Email.Password) + dest.Email.MailFromAddr = overlayString(loaded.Email.MailFromAddr, defaults.Email.MailFromAddr) + dest.Email.MailFromName = overlayString(loaded.Email.MailFromName, defaults.Email.MailFromName) dest.Email.Signature = overlayString(loaded.Email.Signature, defaults.Email.Signature) dest.Email.Disclaimer = overlayString(loaded.Email.Disclaimer, defaults.Email.Disclaimer) dest.Rendering.TemplateDir = overlayString(loaded.Rendering.TemplateDir, defaults.Rendering.TemplateDir) diff --git a/config/default.yaml b/config/default.yaml index 9cd9d3d..1cf7e58 100644 --- a/config/default.yaml +++ b/config/default.yaml @@ -23,6 +23,8 @@ email: authType: plain user: jrn password: foobiebletch + mailFromAddr: "nobody@example.com" + mailFromName: "Amsterdam E-Mail Service" signature: |- Amsterdam - community services, conferencing and more. disclaimer: |- diff --git a/database/contactinfo.go b/database/contactinfo.go new file mode 100644 index 0000000..c5e8a47 --- /dev/null +++ b/database/contactinfo.go @@ -0,0 +1,102 @@ +/* + * Amsterdam Web Communities System + * Copyright (c) 2025 Erbosoft Metaverse Design Solutions, All Rights Reserved + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ +// The database package contains database management and storage logic. +package database + +import ( + "fmt" + "sync" + "time" + + lru "github.com/hashicorp/golang-lru" +) + +// ContactInfo stores the contact information for a user or community. +type ContactInfo struct { + ContactId int32 `db:"contactid"` + GivenName string `db:"given_name"` + FamilyName string `db:"family_name"` + MiddleInit string `db:"middle_init"` + Prefix *string `db:"prefix"` + Suffix *string `db:"suffix"` + Company *string `db:"company"` + Addr1 *string `db:"addr1"` + Addr2 *string `db:"addr2"` + Locality *string `db:"locality"` + Region *string `db:"region"` + PostalCode *string `db:"pcode"` + Country *string `db:"country"` + Phone *string `db:"phone"` + Fax *string `db:"fax"` + Mobile *string `db:"mobile"` + Email *string `db:"email"` + PrivateAddr bool `db:"pvt_addr"` + PrivatePhone bool `db:"pvt_phone"` + PrivateFax bool `db:"pvt_fax"` + PrivateEmail bool `db:"pvt_email"` + OwnerUid int32 `db:"owner_uid"` + OwnerCommId int32 `db:"owner_commid"` + PhotoURL *string `db:"photo_url"` + URL *string `db:"url"` + LastUpdate *time.Time +} + +// contactCache is the cache for ContactInfo objects. +var contactCache *lru.TwoQueueCache = nil + +// getContactMutex is a mutex on AmGetContactInfo. +var getContactMutex sync.Mutex + +// init initializes the contact info cache. +func init() { + var err error + contactCache, err = lru.New2Q(100) + if err != nil { + panic(err) + } +} + +// internalContactInfo retrieves the contact info from the database. +func internalContactInfo(id int32) (*ContactInfo, error) { + var dbdata []ContactInfo + err := amdb.Select(&dbdata, "SELECT * from contacts WHERE contactid = ?", id) + if err == nil { + if len(dbdata) > 1 { + err = fmt.Errorf("internalContactInfo(%d): Too many responses (%d)", id, len(dbdata)) + } else if len(dbdata) == 0 { + return nil, nil + } else { + return &(dbdata[0]), nil + } + } + return nil, err +} + +/* AmGetContactInfo retrieves the contact info for a given identifier. + * Parameters: + * id - The contact info ID top retrieve. + * Returns: + * + */ +func AmGetContactInfo(id int32) (*ContactInfo, error) { + getContactMutex.Lock() + defer getContactMutex.Unlock() + rc, ok := contactCache.Get(id) + if ok { + return rc.(*ContactInfo), nil + } + rc2, err := internalContactInfo(id) + if err == nil { + if rc2 != nil { + contactCache.Add(id, rc2) + } + return rc2, nil + } + return nil, err +} diff --git a/database/user.go b/database/user.go index 44df3e9..19196f7 100644 --- a/database/user.go +++ b/database/user.go @@ -60,6 +60,14 @@ func init() { } } +// ContactInfo returns the contact info structure for the user. +func (u *User) ContactInfo() (*ContactInfo, error) { + if u.ContactID < 0 { + return nil, nil + } + return AmGetContactInfo(u.ContactID) +} + /* AmGetUser returns a reference to the specified user. * Parameters: * uid - The UID of the user. diff --git a/email/message.go b/email/message.go index 3911281..ea64eb5 100644 --- a/email/message.go +++ b/email/message.go @@ -13,6 +13,7 @@ package email import ( "fmt" + "git.erbosoft.com/amy/amsterdam/config" "git.erbosoft.com/amy/amsterdam/util" "github.com/CloudyKit/jet/v6" ) @@ -126,6 +127,7 @@ func AmNewEmailMessage(sender int32, ip string) Message { } rc.uid = sender rc.ip = ip + rc.SetFrom(config.GlobalConfig.Email.MailFromAddr, config.GlobalConfig.Email.MailFromName) return rc } diff --git a/email/templates/pass_remind.jet b/email/templates/pass_remind.jet index cb4b7af..bc28588 100644 --- a/email/templates/pass_remind.jet +++ b/email/templates/pass_remind.jet @@ -7,7 +7,7 @@ If this reminder is not sufficient for you to remember what your password is, then the system can change your password for you. To do so, please visit the following URL: -http://example.com/passrecovery/{{ change_uid }}.{{ change.auth }} +http://example.com/passrecovery/{{ change_uid }}.{{ change_auth }} Your password will be changed and a new password will be E-mailed to you at this address. diff --git a/login.go b/login.go index cc13054..96bb3cc 100644 --- a/login.go +++ b/login.go @@ -10,9 +10,11 @@ package main import ( + "errors" "fmt" "git.erbosoft.com/amy/amsterdam/database" + "git.erbosoft.com/amy/amsterdam/email" "git.erbosoft.com/amy/amsterdam/ui" ) @@ -72,13 +74,30 @@ func Login(ctxt ui.AmContext) (string, any, error) { if action == "remind" { // Password Reminder button pressed user, uerr := database.AmGetUserByName(dlg.Field("user").Value) if uerr == nil { - _ = user - // TODO: send password reminder - + var ci *database.ContactInfo + ci, uerr = user.ContactInfo() + if uerr == nil { + if ci != nil && ci.Email != nil && *ci.Email != "" { + msg := email.AmNewEmailMessage(user.Uid, ctxt.RemoteIP()) + msg.AddTo(*ci.Email, "") + msg.SetTemplate("pass_remind.jet") + msg.AddVariable("username", user.Username) + msg.AddVariable("reminder", user.PassReminder) + msg.AddVariable("change_uid", user.Uid) + msg.AddVariable("change_auth", "TODO") // TODO: add change auth link + msg.Send() + } else { + uerr = errors.New("cannot find email address") + } + } } dlg.Field("pass").Value = "" - return dlg.RenderError(ctxt, "Password reminder has been sent to your E-mail address.") + if uerr == nil { + return dlg.RenderInfo(ctxt, "Password reminder has been sent to your E-mail address.") + } else { + return dlg.RenderError(ctxt, uerr.Error()) + } } if action == "login" { // Login button pressed // authenticate the user diff --git a/ui/dialog.go b/ui/dialog.go index b8ef1ba..e03503b 100644 --- a/ui/dialog.go +++ b/ui/dialog.go @@ -146,6 +146,20 @@ func (d *Dialog) RenderError(ctxt AmContext, errormessage string) (string, any, return d.Render(ctxt) } +/* RenderInfo sets up the rendering parameters to send this dialog to the output with an info message. + * Parameters: + * ctxt - The AmContext for this request. + * infoMessage - The info message to be displayed. + * Returns: + * Command string dictating what to be rendered. + * Data as a parameter for the command string. + * Standard Go error status. + */ +func (d *Dialog) RenderInfo(ctxt AmContext, infoMessage string) (string, any, error) { + ctxt.VarMap().Set("amsterdam_infoMessage", infoMessage) + return d.Render(ctxt) +} + /* LoadFromForm loads the values in a dialog from the form fields in the request. * Parameters: * ctxt - The AmContext for this request. diff --git a/ui/views/dialog.jet b/ui/views/dialog.jet index 9786cf1..7e89e11 100644 --- a/ui/views/dialog.jet +++ b/ui/views/dialog.jet @@ -38,6 +38,19 @@ {{ end }} + {{ if isset(amsterdam_infoMessage) }} + + + {{ end }}