Skip to content

Commit

Permalink
Send mails in chunks (#1509)
Browse files Browse the repository at this point in the history
* Send mails to groups in chunks

* Send mails to all members in chunks

* Inline variable

* Use const

* Deduplicate recipients in chunked delivery

* Remove unnecessary test setup

* Use async/await

* Ensure correct handling of duplicated recipients

* Allow to take sender from Message

* fix after merge

* solution for the member question

* final adaptions

---------

Co-authored-by: leider <derleider@web.de>
  • Loading branch information
UrsMetz and leider authored Dec 8, 2024
1 parent d3f2db2 commit 4539ccd
Show file tree
Hide file tree
Showing 7 changed files with 411 additions and 42 deletions.
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";
},
};
59 changes: 54 additions & 5 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 groupsService = require("../groups/groupsService");
Expand All @@ -16,6 +17,7 @@ const Group = require("../groups/group");
const misc = require("../commons/misc");

const mailtransport = require("./mailtransport");
const statusmessage = require("../commons/statusmessage");

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

function createChunkedSendingReportMessage(statusmessages, subject, message) {
const isError = R.any(statusmessage.isErrorMessage, statusmessages);
const markdown = isError
? `Fehler: ${statusmessages.map((s) => s.contents().additionalArguments.err).join(" ")}`
: "E-Mails erfolgreich versendet";

return message.cloneWithBody({
subject: `${isError ? "ERROR" : "SUCCESS"} ${subject}`,
markdown,
sendCopyToSelf: true,
});
}

async function sendMailInChunks(maxMailSendingChunkSize, allMembers, message, type) {
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}"`, message);
await sendMail(resultMessage, type);
} catch (err) {
logger.error(err);
}
}

module.exports = {
activityMarkdown,

dataForShowingMessageForActivity: async function (activityURL, language) {
dataForShowingMessageForActivity: async function dataForShowingMessageForActivity(activityURL, language) {
const [activity, groups] = [
activitiesService.getActivityWithGroupAndParticipants(activityURL),
groupstore.allGroups(),
Expand Down Expand Up @@ -105,7 +137,11 @@ module.exports = {
}
try {
groups.forEach(groupsAndMembersService.addMembersToGroup);
message.setBccToGroupMemberAddresses(groups);
const allMembers = R.uniqBy(
(member) => member.email(),
groups.flatMap((group) => group.members),
);

let activity;
try {
activity = activitystore.getActivity(activityURL);
Expand All @@ -115,7 +151,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);

return mailtransport.statusmessageForSuccess(type);
} else {
message.setBccToMemberAddresses(allMembers);
return sendMail(message, type);
}
} catch (err1) {
return mailtransport.statusmessageForError(type, err1);
}
Expand All @@ -142,8 +188,11 @@ module.exports = {
sendMailToAllMembers: async function sendMailToAllMembers(message) {
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);

return mailtransport.statusmessageForSuccess(type);
},

sendMagicLinkToMember: async function sendMagicLinkToMember(member, token) {
Expand Down
8 changes: 8 additions & 0 deletions softwerkskammer/lib/mailsender/message.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ class Message {
return this;
}

cloneWithBody(body) {
const fakeMember = {
displayName: () => this.senderName,
email: () => this.senderAddress,
};
return new Message(body, fakeMember);
}

setTo(toAddresses) {
this.to = toAddresses;
}
Expand Down
Loading

0 comments on commit 4539ccd

Please sign in to comment.