Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Send mails in chunks #1509

Merged
merged 13 commits into from
Dec 8, 2024
3 changes: 2 additions & 1 deletion config-examples/mailsender-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,6 @@
"sender-name": "Softwerkskammer Benachrichtigungen",
"sender-address": "info@my-domain.org",
"include-footer": false,
"doNotSendMails": "insert email address here for local development as replacement for receivers"
"doNotSendMails": "insert email address here for local development as replacement for receivers",
"maxMailSendingChunkSize": 3
}
4 changes: 4 additions & 0 deletions softwerkskammer/lib/commons/statusmessage.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,8 @@ module.exports = {
successMessage: function successMessage(title, text, additionalArguments) {
return statusMessage("alert-success", title, text, additionalArguments);
},

isErrorMessage: function isErrorMessage(statusmessage) {
return statusmessage.contents().type === "alert-danger";
},
};
6 changes: 3 additions & 3 deletions softwerkskammer/lib/mailsender/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,19 @@ async function messageSubmitted(req, res) {

async function doTheRightSending() {
if (req.body.massMailing === "members") {
return mailsenderService.sendMailToAllMembers(message);
return mailsenderService.sendMailToAllMembers(message, req.user.member);
}
const activityURL = req.body.successURL.replace("/activities/", "");
if (req.body.toParticipants) {
message.removeAllButFirstButton();
return mailsenderService.sendMailToParticipantsOf(activityURL, message);
}
if (req.body.invitedGroups) {
return mailsenderService.sendMailToInvitedGroups(req.body.invitedGroups, activityURL, message);
return mailsenderService.sendMailToInvitedGroups(req.body.invitedGroups, activityURL, message, req.user.member);
}
if (req.body.groupName) {
message.subject = `[${req.body.emailPrefix}] ${message.subject}`;
return mailsenderService.sendMailToInvitedGroups([req.body.groupName], undefined, message);
return mailsenderService.sendMailToInvitedGroups([req.body.groupName], undefined, message, req.user.member);
}
if (req.body.nickname) {
return mailsenderService.sendMailToMember(req.body.nickname, message);
Expand Down
63 changes: 57 additions & 6 deletions softwerkskammer/lib/mailsender/mailsenderService.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"use strict";
const { DateTime } = require("luxon");
const conf = require("simple-configure");
const R = require("ramda");
const logger = require("winston").loggers.get("application");

const beans = conf.get("beans");
Expand All @@ -17,6 +18,7 @@ const Group = beans.get("group");
const misc = beans.get("misc");

const mailtransport = beans.get("mailtransport");
const statusmessage = beans.get("statusmessage");

async function sendMail(message, type) {
return mailtransport.sendMail(message, type, conf.get("sender-address"), conf.get("include-footer"));
Expand All @@ -38,6 +40,38 @@ function activityMarkdown(activity, language) {
return markdown;
}

function createChunkedSendingReportMessage(statusmessages, subject, sender) {
const anySendingError = R.any(statusmessage.isErrorMessage, statusmessages);
const resultMessage = new Message(
{
subject,
markdown: anySendingError
? `Fehler: ${statusmessages.map((s) => s.contents().additionalArguments.err).join(" ")}`
: "E-Mails erfolgreich versendet",
},
sender,
);
resultMessage.setTo(sender.email());
return resultMessage;
}

async function sendMailInChunks(maxMailSendingChunkSize, allMembers, message, type, sender) {
const membersInChunks = R.splitEvery(maxMailSendingChunkSize, allMembers);

try {
const statusmessages = await Promise.all(
membersInChunks.map((bccs) => {
message.setBccToMemberAddresses(bccs);
return sendMail(message, type);
}),
);
const resultMessage = createChunkedSendingReportMessage(statusmessages, `Report "${message.subject}"`, sender);
await sendMail(resultMessage, type);
} catch (err) {
logger.error(err);
}
}

module.exports = {
activityMarkdown,

Expand Down Expand Up @@ -97,7 +131,7 @@ module.exports = {
}
},

sendMailToInvitedGroups: async function sendMailToInvitedGroups(invitedGroups, activityURL, message) {
sendMailToInvitedGroups: async function sendMailToInvitedGroups(invitedGroups, activityURL, message, sender) {
const type = "$t(mailsender.invitation)";
try {
const groups = groupsService.getGroups(invitedGroups);
Expand All @@ -106,7 +140,11 @@ module.exports = {
}
try {
groups.forEach(groupsAndMembersService.addMembersToGroup);
message.setBccToGroupMemberAddresses(groups);
const allMembers = R.uniqBy(
leider marked this conversation as resolved.
Show resolved Hide resolved
(member) => member.email(),
groups.flatMap((group) => group.members),
);

UrsMetz marked this conversation as resolved.
Show resolved Hide resolved
let activity;
try {
activity = activitystore.getActivity(activityURL);
Expand All @@ -116,7 +154,17 @@ module.exports = {
if (activity) {
message.setIcal(icalService.activityAsICal(activity).toString());
}
return sendMail(message, type);

const maxMailSendingChunkSize = conf.get("maxMailSendingChunkSize");
if (allMembers.length > maxMailSendingChunkSize) {
// noinspection ES6MissingAwait - we explicitly don't want to wait because that could cause timeouts
sendMailInChunks(maxMailSendingChunkSize, allMembers, message, type, sender);

return mailtransport.statusmessageForSuccess();
} else {
message.setBccToMemberAddresses(allMembers);
return sendMail(message, type);
}
} catch (err1) {
return mailtransport.statusmessageForError(type, err1);
}
Expand All @@ -140,11 +188,14 @@ module.exports = {
}
},

sendMailToAllMembers: async function sendMailToAllMembers(message) {
sendMailToAllMembers: async function sendMailToAllMembers(message, sender) {
leider marked this conversation as resolved.
Show resolved Hide resolved
const type = "$t(mailsender.notification)";
const members = memberstore.allMembers();
message.setBccToMemberAddresses(members);
return sendMail(message, type);

// noinspection ES6MissingAwait - we explicitly don't want to wait because that could cause timeouts
sendMailInChunks(conf.get("maxMailSendingChunkSize"), members, message, type, sender);

return mailtransport.statusmessageForSuccess();
},

sendMagicLinkToMember: async function sendMagicLinkToMember(member, token) {
Expand Down
Loading