diff --git a/communityadmin.go b/communityadmin.go index bc6225d..9a86dca 100644 --- a/communityadmin.go +++ b/communityadmin.go @@ -17,8 +17,10 @@ import ( "net/http" "strconv" "strings" + "time" "git.erbosoft.com/amy/amsterdam/database" + "git.erbosoft.com/amy/amsterdam/email" "git.erbosoft.com/amy/amsterdam/ui" "git.erbosoft.com/amy/amsterdam/util" "github.com/labstack/echo/v4" @@ -479,6 +481,77 @@ func CommunityCategory(ctxt ui.AmContext) (string, any) { return "framed", "comm_category.jet" } +/* CommunityEmailForm displays the form for sending mass mail to the community. + * Parameters: + * ctxt - The AmContext for the request. + * Returns: + * Command string dictating what to be rendered. + * Data as a parameter for the command string. + * Standard Go error status. + */ +func CommunityEmailForm(ctxt ui.AmContext) (string, any) { + comm := ctxt.CurrentCommunity() + if !comm.TestPermission("Community.MassMail", ctxt.EffectiveLevel()) { + return "error", ENOACCESS + } + + ctxt.VarMap().Set("commName", comm.Name) + ctxt.VarMap().Set("selfLink", fmt.Sprintf("/comm/%s/admin/massmail", comm.Alias)) + ctxt.VarMap().Set("subj", "") + ctxt.VarMap().Set("pb", "") + ctxt.SetFrameTitle("Community E-Mail: " + comm.Name) + return "framed", "comm_email.jet" +} + +func CommunityEmail(ctxt ui.AmContext) (string, any) { + comm := ctxt.CurrentCommunity() + if !comm.TestPermission("Community.MassMail", ctxt.EffectiveLevel()) { + return "error", ENOACCESS + } + + // Handle button presses. + if ctxt.FormFieldIsSet("cancel") { + return "redirect", fmt.Sprintf("/comm/%s/admin", comm.Alias) + } else if !ctxt.FormFieldIsSet("send") { + return "error", EBUTTON + } + + recipients, err := comm.GetMemberEMailAddrs(ctxt.Ctx()) + if err != nil { + return "error", err + } + + // Kick off a background task to send all the E-mail messages. + subj := ctxt.FormField("subj") + pb := ctxt.FormField("pb") + commName := comm.Name + myUID := ctxt.CurrentUserId() + myIP := ctxt.RemoteIP() + log.Infof("CommunityEmail: About to send mass E-mail to %d recipients", len(recipients)) + ampool.Submit(func(ctx context.Context) { + start := time.Now() + RunLoop: + for _, addr := range recipients { + select { + case <-ctx.Done(): + break RunLoop + default: + msg := email.AmNewEmailMessage(myUID, myIP) + msg.AddTo(addr, "") + msg.SetSubject(subj) + msg.SetTemplate("comm_massmail.jet") + msg.AddVariable("text", pb) + msg.AddVariable("commName", commName) + msg.Send() + } + } + elapsed := time.Since(start) + log.Infof("CommunityEmail delivery completed in %s", elapsed) + }) + + return "redirect", fmt.Sprintf("/comm/%s/admin", comm.Alias) +} + /* CreateCommunityForm renders the form for creating a new community. * Parameters: * ctxt - The AmContext for the request. diff --git a/conferenceadmin.go b/conferenceadmin.go index 809d3f5..41795ce 100644 --- a/conferenceadmin.go +++ b/conferenceadmin.go @@ -622,20 +622,22 @@ func ConferenceEmail(ctxt ui.AmContext) (string, any) { log.Infof("ConferenceEmail: About to send mass E-mail to %d recipients", len(recipients)) ampool.Submit(func(ctx context.Context) { start := time.Now() + RunLoop: for _, addr := range recipients { - err := ctx.Err() - if err != nil { - break + select { + case <-ctx.Done(): + break RunLoop + default: + msg := email.AmNewEmailMessage(myUID, myIP) + msg.AddTo(addr, "") + msg.SetSubject(subj) + msg.SetTemplate(templateName) + msg.AddVariable("text", pb) + msg.AddVariable("topicName", topicName) + msg.AddVariable("confName", confName) + msg.AddVariable("commName", commName) + msg.Send() } - msg := email.AmNewEmailMessage(myUID, myIP) - msg.AddTo(addr, "") - msg.SetSubject(subj) - msg.SetTemplate(templateName) - msg.AddVariable("text", pb) - msg.AddVariable("topicName", topicName) - msg.AddVariable("confName", confName) - msg.AddVariable("commName", commName) - msg.Send() } elapsed := time.Since(start) log.Infof("ConferenceEmail delivery completed in %s", elapsed) diff --git a/database/community.go b/database/community.go index 7b58dee..c9698b0 100644 --- a/database/community.go +++ b/database/community.go @@ -583,6 +583,15 @@ func (c *Community) TouchUpdate(ctx context.Context) error { return err } +// GetMemberEmailAddrs gets the E-mail address of all community members, except those that have opted out. +func (c *Community) GetMemberEMailAddrs(ctx context.Context) ([]string, error) { + sql := fmt.Sprintf(`SELECT c.email FROM contacts c, users u, commmember m, propuser p WHERE c.contactid = u.contactid AND u.uid = m.uid AND m.commid = ? + AND u.is_anon = 0 AND u.uid = p.uid AND p.ndx = %d AND p.data NOT LIKE '%%%s%%'`, UserPropFlags, util.OptionCharFromIndex(UserFlagMassMailOptOut)) + var rc []string + err := amdb.SelectContext(ctx, &rc, sql, c.Id) + return rc, err +} + /* AmGetCommunity returns a reference to the specified community. * Parameters: * ctx - Standard Go context value. diff --git a/docs/MISSINGFUNCS.md b/docs/MISSINGFUNCS.md index 63e2c5d..7aa3aad 100644 --- a/docs/MISSINGFUNCS.md +++ b/docs/MISSINGFUNCS.md @@ -26,7 +26,7 @@ _(italicized items can be deferred)_ - Community Admin Menu: - ~~Set Community Category~~ - Membership Control - - E-Mail to All Members + - ~~E-Mail to All Members~~ - ~~Display Audit Records~~ - Delete Community - ~~Community Profile: Invite~~ diff --git a/email/templates/comm_massmail.jet b/email/templates/comm_massmail.jet new file mode 100644 index 0000000..553b3b4 --- /dev/null +++ b/email/templates/comm_massmail.jet @@ -0,0 +1,16 @@ +{* + * Amsterdam Web Communities System + * Copyright (c) 2025-2026 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/. + *} +{{ text }} + +You are receiving this message because you are a member of the Amsterdam +community "${community.name}". The sender is a host of this community. +To stop receiving mass E-mailed notices from all Amsterdam community and +conference hosts, visit Amsterdam, click on the "Profile" link, check the box +labeled "Don't send me mass E-mail from community/conference hosts," and +click Update to save this preference. diff --git a/main.go b/main.go index e922b2f..c51a57e 100644 --- a/main.go +++ b/main.go @@ -76,24 +76,25 @@ func setupEcho() *echo.Echo { e.GET("/hotlist", ui.AmWrap(Hotlist)) e.GET("/sideboxes", ui.AmWrap(ManageSideboxes)) e.POST("/sideboxes", ui.AmWrap(AddSidebox)) - e.GET("/sysadmin", ui.AmWrap(SysAdminMenu)) - e.GET("/sysadmin/globals", ui.AmWrap(GlobalPropertiesForm)) - e.POST("/sysadmin/globals", ui.AmWrap(GlobalPropertiesSet)) - e.Match(GetAndPost, "/sysadmin/users", ui.AmWrap(UserManagementSearch)) - e.GET("/sysadmin/users/:uname", ui.AmWrap(UserManagementForm)) - e.POST("/sysadmin/users/:uname", ui.AmWrap(UserManagementSave)) - e.GET("/sysadmin/users/:uname/photo", ui.AmWrap(AdminUserPhotoForm)) - e.POST("/sysadmin/users/:uname/photo", ui.AmWrap(AdminUserPhoto)) - e.GET("/sysadmin/ipban", ui.AmWrap(IPBanList)) - e.GET("/sysadmin/ipban/add", ui.AmWrap(AddIPBanForm)) - e.Match(GetAndPost, "/sysadmin/audit", ui.AmWrap(SystemAudit)) - e.POST("/sysadmin/ipban/add", ui.AmWrap(AddIPBan)) e.GET("/create_comm", ui.AmWrap(CreateCommunityForm)) e.POST("/create_comm", ui.AmWrap(CreateCommunity)) e.GET("/manage_comm", ui.AmWrap(ManageCommunities)) e.POST("/attachment_upload", ui.AmWrap(AttachmentUpload)) e.GET("/attachment/:post", ui.AmWrap(AttachmentSend)) e.POST("/__invite_send", ui.AmWrap(InviteSend)) + sysGroup := e.Group("/sysadmin") + sysGroup.GET("", ui.AmWrap(SysAdminMenu)) + sysGroup.GET("/globals", ui.AmWrap(GlobalPropertiesForm)) + sysGroup.POST("/globals", ui.AmWrap(GlobalPropertiesSet)) + sysGroup.Match(GetAndPost, "/users", ui.AmWrap(UserManagementSearch)) + sysGroup.GET("/users/:uname", ui.AmWrap(UserManagementForm)) + sysGroup.POST("/users/:uname", ui.AmWrap(UserManagementSave)) + sysGroup.GET("/users/:uname/photo", ui.AmWrap(AdminUserPhotoForm)) + sysGroup.POST("/users/:uname/photo", ui.AmWrap(AdminUserPhoto)) + sysGroup.GET("/ipban", ui.AmWrap(IPBanList)) + sysGroup.GET("/ipban/add", ui.AmWrap(AddIPBanForm)) + sysGroup.POST("/ipban/add", ui.AmWrap(AddIPBan)) + sysGroup.Match(GetAndPost, "/audit", ui.AmWrap(SystemAudit)) // community group commGroup := e.Group("/comm/:cid", ui.SetCommunity) @@ -110,13 +111,16 @@ func setupEcho() *echo.Echo { commGroup.GET("/invite", ui.AmWrap(InviteToCommunity)) commGroup.GET("/find", ui.AmWrap(FindPostsPageCommunity)) commGroup.POST("/find", ui.AmWrap(FindPostsCommunity)) - commGroup.GET("/admin", ui.AmWrap(CommunityAdminMenu)) - commGroup.GET("/admin/profile", ui.AmWrap(CommunityProfileForm)) - commGroup.POST("/admin/profile", ui.AmWrap(EditCommunityProfile)) - commGroup.GET("/admin/logo", ui.AmWrap(CommunityLogoForm)) - commGroup.POST("/admin/logo", ui.AmWrap(EditCommunityLogo)) - commGroup.Match(GetAndPost, "/admin/audit", ui.AmWrap(CommunityAudit)) - commGroup.GET("/admin/category", ui.AmWrap(CommunityCategory)) + adminGroup := commGroup.Group("/admin") + adminGroup.GET("", ui.AmWrap(CommunityAdminMenu)) + adminGroup.GET("/profile", ui.AmWrap(CommunityProfileForm)) + adminGroup.POST("/profile", ui.AmWrap(EditCommunityProfile)) + adminGroup.GET("/logo", ui.AmWrap(CommunityLogoForm)) + adminGroup.POST("/logo", ui.AmWrap(EditCommunityLogo)) + adminGroup.Match(GetAndPost, "/audit", ui.AmWrap(CommunityAudit)) + adminGroup.GET("/category", ui.AmWrap(CommunityCategory)) + adminGroup.GET("/massmail", ui.AmWrap(CommunityEmailForm)) + adminGroup.POST("/massmail", ui.AmWrap(CommunityEmail)) // conference group commGroup.GET("/create_conf", ui.AmWrap(CreateConferenceForm)) diff --git a/ui/menudefs.yaml b/ui/menudefs.yaml index 8b8311e..8291258 100644 --- a/ui/menudefs.yaml +++ b/ui/menudefs.yaml @@ -75,7 +75,7 @@ menudefs: link: "/TODO/comm/[CID]/admin/members" permission: "Community.ShowAdmin" - text: "E-Mail to All Members" - link: "/TODO/comm/[CID]/admin/massmail" + link: "/comm/[CID]/admin/massmail" permission: "Community.MassMail" - text: "Display Audit Records" link: "/comm/[CID]/admin/audit" diff --git a/ui/views/comm_email.jet b/ui/views/comm_email.jet new file mode 100644 index 0000000..971b145 --- /dev/null +++ b/ui/views/comm_email.jet @@ -0,0 +1,82 @@ +{* + * Amsterdam Web Communities System + * Copyright (c) 2025-2026 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/. + *} +
+ +
+
+

Community E-Mail:

+

{{ commName }}

+
+
+
+ + +
+
+ ⚠️ +
+

Administrator Notice:

+

You are about to send an E-mail to all community members. Please use this feature responsibly and avoid spamming community members unnecessarily.

+
+
+
+ + +
+
+ +
+
+ 📧 + Send E-mail to all members of the community: +
+ + +

+ ✉️ + Compose Message +

+ + +
+ + +
+ + +
+ + +
+
+ + +
+ + +
+
+
+