landed E-mail to all Communtiy Members
This commit is contained in:
@@ -17,8 +17,10 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.erbosoft.com/amy/amsterdam/database"
|
"git.erbosoft.com/amy/amsterdam/database"
|
||||||
|
"git.erbosoft.com/amy/amsterdam/email"
|
||||||
"git.erbosoft.com/amy/amsterdam/ui"
|
"git.erbosoft.com/amy/amsterdam/ui"
|
||||||
"git.erbosoft.com/amy/amsterdam/util"
|
"git.erbosoft.com/amy/amsterdam/util"
|
||||||
"github.com/labstack/echo/v4"
|
"github.com/labstack/echo/v4"
|
||||||
@@ -479,6 +481,77 @@ func CommunityCategory(ctxt ui.AmContext) (string, any) {
|
|||||||
return "framed", "comm_category.jet"
|
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.
|
/* CreateCommunityForm renders the form for creating a new community.
|
||||||
* Parameters:
|
* Parameters:
|
||||||
* ctxt - The AmContext for the request.
|
* ctxt - The AmContext for the request.
|
||||||
|
|||||||
+14
-12
@@ -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))
|
log.Infof("ConferenceEmail: About to send mass E-mail to %d recipients", len(recipients))
|
||||||
ampool.Submit(func(ctx context.Context) {
|
ampool.Submit(func(ctx context.Context) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
RunLoop:
|
||||||
for _, addr := range recipients {
|
for _, addr := range recipients {
|
||||||
err := ctx.Err()
|
select {
|
||||||
if err != nil {
|
case <-ctx.Done():
|
||||||
break
|
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)
|
elapsed := time.Since(start)
|
||||||
log.Infof("ConferenceEmail delivery completed in %s", elapsed)
|
log.Infof("ConferenceEmail delivery completed in %s", elapsed)
|
||||||
|
|||||||
@@ -583,6 +583,15 @@ func (c *Community) TouchUpdate(ctx context.Context) error {
|
|||||||
return err
|
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.
|
/* AmGetCommunity returns a reference to the specified community.
|
||||||
* Parameters:
|
* Parameters:
|
||||||
* ctx - Standard Go context value.
|
* ctx - Standard Go context value.
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ _(italicized items can be deferred)_
|
|||||||
- Community Admin Menu:
|
- Community Admin Menu:
|
||||||
- ~~Set Community Category~~
|
- ~~Set Community Category~~
|
||||||
- Membership Control
|
- Membership Control
|
||||||
- E-Mail to All Members
|
- ~~E-Mail to All Members~~
|
||||||
- ~~Display Audit Records~~
|
- ~~Display Audit Records~~
|
||||||
- Delete Community
|
- Delete Community
|
||||||
- ~~Community Profile: Invite~~
|
- ~~Community Profile: Invite~~
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -76,24 +76,25 @@ func setupEcho() *echo.Echo {
|
|||||||
e.GET("/hotlist", ui.AmWrap(Hotlist))
|
e.GET("/hotlist", ui.AmWrap(Hotlist))
|
||||||
e.GET("/sideboxes", ui.AmWrap(ManageSideboxes))
|
e.GET("/sideboxes", ui.AmWrap(ManageSideboxes))
|
||||||
e.POST("/sideboxes", ui.AmWrap(AddSidebox))
|
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.GET("/create_comm", ui.AmWrap(CreateCommunityForm))
|
||||||
e.POST("/create_comm", ui.AmWrap(CreateCommunity))
|
e.POST("/create_comm", ui.AmWrap(CreateCommunity))
|
||||||
e.GET("/manage_comm", ui.AmWrap(ManageCommunities))
|
e.GET("/manage_comm", ui.AmWrap(ManageCommunities))
|
||||||
e.POST("/attachment_upload", ui.AmWrap(AttachmentUpload))
|
e.POST("/attachment_upload", ui.AmWrap(AttachmentUpload))
|
||||||
e.GET("/attachment/:post", ui.AmWrap(AttachmentSend))
|
e.GET("/attachment/:post", ui.AmWrap(AttachmentSend))
|
||||||
e.POST("/__invite_send", ui.AmWrap(InviteSend))
|
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
|
// community group
|
||||||
commGroup := e.Group("/comm/:cid", ui.SetCommunity)
|
commGroup := e.Group("/comm/:cid", ui.SetCommunity)
|
||||||
@@ -110,13 +111,16 @@ func setupEcho() *echo.Echo {
|
|||||||
commGroup.GET("/invite", ui.AmWrap(InviteToCommunity))
|
commGroup.GET("/invite", ui.AmWrap(InviteToCommunity))
|
||||||
commGroup.GET("/find", ui.AmWrap(FindPostsPageCommunity))
|
commGroup.GET("/find", ui.AmWrap(FindPostsPageCommunity))
|
||||||
commGroup.POST("/find", ui.AmWrap(FindPostsCommunity))
|
commGroup.POST("/find", ui.AmWrap(FindPostsCommunity))
|
||||||
commGroup.GET("/admin", ui.AmWrap(CommunityAdminMenu))
|
adminGroup := commGroup.Group("/admin")
|
||||||
commGroup.GET("/admin/profile", ui.AmWrap(CommunityProfileForm))
|
adminGroup.GET("", ui.AmWrap(CommunityAdminMenu))
|
||||||
commGroup.POST("/admin/profile", ui.AmWrap(EditCommunityProfile))
|
adminGroup.GET("/profile", ui.AmWrap(CommunityProfileForm))
|
||||||
commGroup.GET("/admin/logo", ui.AmWrap(CommunityLogoForm))
|
adminGroup.POST("/profile", ui.AmWrap(EditCommunityProfile))
|
||||||
commGroup.POST("/admin/logo", ui.AmWrap(EditCommunityLogo))
|
adminGroup.GET("/logo", ui.AmWrap(CommunityLogoForm))
|
||||||
commGroup.Match(GetAndPost, "/admin/audit", ui.AmWrap(CommunityAudit))
|
adminGroup.POST("/logo", ui.AmWrap(EditCommunityLogo))
|
||||||
commGroup.GET("/admin/category", ui.AmWrap(CommunityCategory))
|
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
|
// conference group
|
||||||
commGroup.GET("/create_conf", ui.AmWrap(CreateConferenceForm))
|
commGroup.GET("/create_conf", ui.AmWrap(CreateConferenceForm))
|
||||||
|
|||||||
+1
-1
@@ -75,7 +75,7 @@ menudefs:
|
|||||||
link: "/TODO/comm/[CID]/admin/members"
|
link: "/TODO/comm/[CID]/admin/members"
|
||||||
permission: "Community.ShowAdmin"
|
permission: "Community.ShowAdmin"
|
||||||
- text: "E-Mail to All Members"
|
- text: "E-Mail to All Members"
|
||||||
link: "/TODO/comm/[CID]/admin/massmail"
|
link: "/comm/[CID]/admin/massmail"
|
||||||
permission: "Community.MassMail"
|
permission: "Community.MassMail"
|
||||||
- text: "Display Audit Records"
|
- text: "Display Audit Records"
|
||||||
link: "/comm/[CID]/admin/audit"
|
link: "/comm/[CID]/admin/audit"
|
||||||
|
|||||||
@@ -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/.
|
||||||
|
*}
|
||||||
|
<div class="p-4">
|
||||||
|
<!-- Page Title -->
|
||||||
|
<div class="mb-6">
|
||||||
|
<div class="flex items-baseline gap-3 mb-2">
|
||||||
|
<h1 class="text-blue-800 text-4xl font-bold">Community E-Mail:</h1>
|
||||||
|
<h2 class="text-blue-800 text-2xl font-bold">{{ commName }}</h2>
|
||||||
|
</div>
|
||||||
|
<hr class="border-2 border-gray-400 w-4/5 mb-6">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Warning Box -->
|
||||||
|
<div class="bg-yellow-50 border-l-4 border-yellow-400 p-4 mb-6 rounded max-w-4xl">
|
||||||
|
<div class="flex items-start">
|
||||||
|
<span class="text-2xl mr-3">⚠️</span>
|
||||||
|
<div class="text-sm text-yellow-900">
|
||||||
|
<p class="font-bold mb-1">Administrator Notice:</p>
|
||||||
|
<p>You are about to send an E-mail to <i>all</i> community members. Please use this feature responsibly and avoid spamming community members unnecessarily.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email Form -->
|
||||||
|
<form method="POST" action="{{ selfLink }}">
|
||||||
|
<div class="max-w-4xl space-y-6">
|
||||||
|
<!-- Recipient Selection -->
|
||||||
|
<div class="bg-gray-50 border border-gray-300 rounded-lg p-6">
|
||||||
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<span class="text-xl">📧</span>
|
||||||
|
<span class="text-gray-700 font-bold">Send E-mail to all members of the community:</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Email Content -->
|
||||||
|
<h3 class="text-lg font-bold text-gray-800 mb-4 flex items-center gap-2">
|
||||||
|
<span class="text-xl">✉️</span>
|
||||||
|
Compose Message
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<!-- Subject Line -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<label class="block text-gray-700 font-semibold mb-2">
|
||||||
|
Subject:
|
||||||
|
</label>
|
||||||
|
<input type="text" name="subj" value="{{ subj }}" maxlength="255" placeholder="Enter email subject..."
|
||||||
|
class="w-full border border-gray-300 rounded px-4 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Message Body -->
|
||||||
|
<div>
|
||||||
|
<label class="block text-gray-700 font-semibold mb-2">Message:</label>
|
||||||
|
<textarea name="pb" rows="10" class="w-full border border-gray-300 rounded px-4 py-3 font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500 resize-y"
|
||||||
|
placeholder="Enter your message here...">{{ pb | raw }}</textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="flex gap-3 pt-4 border-t border-gray-300">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
name="send"
|
||||||
|
class="bg-green-600 hover:bg-green-700 text-white font-bold px-8 py-3 rounded-lg transition-colors shadow-md hover:shadow-lg flex items-center gap-2">
|
||||||
|
<span class="text-xl">📨</span>
|
||||||
|
Send E-Mail
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
name="cancel"
|
||||||
|
class="bg-gray-600 hover:bg-gray-700 text-white font-bold px-6 py-3 rounded-lg transition-colors shadow-md hover:shadow-lg flex items-center gap-2">
|
||||||
|
<span class="text-xl">✗</span>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user