Skip to content

Commit 182ea7b

Browse files
authored
Revert "Merge master (#262)"
This reverts commit 0dc5962.
1 parent 0dc5962 commit 182ea7b

40 files changed

+895
-1019
lines changed

.env.example

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,13 @@ DATABASE_URL=mysql://root:root@localhost/arena
2727
# Used in mail templates
2828
ARENA_WEBSITE=http://localhost:8080
2929

30+
# SMTP server address (smtp://user:password@host(:port)?)
3031
# You can use Nodemailer App (https://nodemailer.com/app/) or mailtrap.io to test the emails
31-
EMAIL_HOST=
32-
EMAIL_PORT=
33-
EMAIL_SECURE=
34-
EMAIL_SENDER_NAME=
35-
EMAIL_SENDER_ADDRESS=
36-
EMAIL_AUTH_USER=
37-
EMAIL_AUTH_PASSWORD=
38-
EMAIL_REJECT_UNAUTHORIZED=
32+
SMTP_URI=smtp://user:pass@address:25/?pool=true&maxConnections=1
33+
GMAIL=false
34+
GMAIL_USERNAME=[email protected]
35+
GMAIL_PASSWORD=
36+
MAX_MAILS_PER_BATCH=100
3937

4038
# Used to give a discount on tickets
4139
PARTNER_MAILS=utt.fr,utc.fr,utbm.fr

src/controllers/admin/emails/index.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
import { Router } from 'express';
22
import getMails from './getMails';
33
import send from './send';
4-
import sendTemplate from './sendTemplate';
5-
import sendCustom from './sendCustom';
64

75
const router = Router();
86

97
router.get('/', getMails);
108
router.post('/', send);
11-
router.post('/template', sendTemplate);
12-
router.post('/custom', sendCustom);
139

1410
export default router;

src/controllers/admin/emails/send.ts

Lines changed: 114 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
/* eslint-disable unicorn/no-nested-ternary */
12
import { NextFunction, Request, Response } from 'express';
23
import Joi from 'joi';
4+
import { badRequest, created } from '../../../utils/responses';
35
import { hasPermission } from '../../../middlewares/authentication';
4-
import { Error as ApiError, MailGeneralQuery } from '../../../types';
6+
import { Error as ApiError, MailQuery } from '../../../types';
57
import { validateBody } from '../../../middlewares/validation';
6-
import { sendGeneralMail } from '../../../services/email';
8+
import { sendEmail, SerializedMail } from '../../../services/email';
9+
import { serialize } from '../../../services/email/serializer';
10+
import database from '../../../services/database';
711
import { getRequestInfo } from '../../../utils/users';
812

913
export default [
@@ -12,7 +16,23 @@ export default [
1216
validateBody(
1317
Joi.object({
1418
preview: Joi.boolean().default(false),
15-
generalMail: Joi.string().required(),
19+
locked: Joi.boolean().optional(),
20+
tournamentId: Joi.string().optional(),
21+
subject: Joi.string().required(),
22+
highlight: Joi.object({
23+
title: Joi.string().required(),
24+
intro: Joi.string().required(),
25+
}).required(),
26+
reason: Joi.string().optional(),
27+
content: Joi.array()
28+
.items(
29+
Joi.object({
30+
title: Joi.string().required(),
31+
components: Joi.array().required(),
32+
}).required(),
33+
)
34+
.required()
35+
.error(new Error(ApiError.MalformedMailBody)),
1636
}).error(
1737
(errors) =>
1838
errors.find((error) => error.message === ApiError.MalformedMailBody) ?? new Error(ApiError.InvalidMailOptions),
@@ -22,13 +42,100 @@ export default [
2242
// Controller
2343
async (request: Request, response: Response, next: NextFunction) => {
2444
try {
25-
const mail = request.body as MailGeneralQuery;
45+
const mail = request.body as MailQuery;
2646
const { user } = getRequestInfo(response);
2747

28-
const nbMailSent = await sendGeneralMail(mail.generalMail, mail.preview ? user : null);
48+
// Find mail adresses to send the mail to
49+
const mails = await database.user
50+
.findMany({
51+
where: {
52+
registerToken: null,
53+
email: {
54+
not: null,
55+
},
56+
...(mail.preview
57+
? {
58+
id: user.id,
59+
}
60+
: {
61+
team: {
62+
...(mail.locked
63+
? {
64+
NOT: {
65+
lockedAt: null,
66+
},
67+
}
68+
: mail.locked === false
69+
? { lockedAt: null }
70+
: {}),
71+
tournamentId: mail.tournamentId,
72+
},
73+
}),
74+
},
75+
select: {
76+
email: true,
77+
},
78+
})
79+
.then((mailWrappers) => mailWrappers.map((mailWrapper) => mailWrapper.email));
2980

30-
// TODO: change return to a created response
31-
return response.json({ message: `Sent ${nbMailSent} emails` });
81+
// Parallelize mails as it may take time
82+
// As every mail is generated on a user basis, we cannot catch
83+
// a mail format error as simply as usual. This is the reason
84+
// why we track the status of all sent mails.
85+
// If all mails are errored due to invalid syntax, it is most
86+
// likely that the sender did a mistake.
87+
const outgoingMails = await Promise.allSettled(
88+
mails.map(async (adress) => {
89+
let mailContent: SerializedMail;
90+
try {
91+
mailContent = await serialize({
92+
sections: mail.content,
93+
reason: mail.reason,
94+
title: {
95+
banner: mail.subject,
96+
highlight: mail.highlight.title,
97+
short: mail.highlight.intro,
98+
topic: mail.preview ? `[PREVIEW]: ${mail.subject}` : mail.subject,
99+
},
100+
receiver: adress,
101+
});
102+
} catch {
103+
throw ApiError.MalformedMailBody;
104+
}
105+
return sendEmail(mailContent);
106+
}),
107+
);
108+
109+
// Counts mail statuses
110+
const results = outgoingMails.reduce(
111+
(result, state) => {
112+
if (state.status === 'fulfilled')
113+
return {
114+
...result,
115+
delivered: result.delivered + 1,
116+
};
117+
if (state.reason === ApiError.MalformedMailBody)
118+
return {
119+
...result,
120+
malformed: result.malformed + 1,
121+
};
122+
return {
123+
...result,
124+
undelivered: result.undelivered + 1,
125+
};
126+
},
127+
{ malformed: 0, delivered: 0, undelivered: 0 },
128+
);
129+
130+
// Respond to the request with the appropriate response code
131+
if (results.malformed && !results.delivered && !results.undelivered)
132+
return badRequest(response, ApiError.MalformedMailBody);
133+
134+
if (results.delivered || !results.undelivered) return created(response, results);
135+
136+
throw (<PromiseRejectedResult>(
137+
outgoingMails.find((result) => result.status === 'rejected' && result.reason !== ApiError.MalformedMailBody)
138+
)).reason;
32139
} catch (error) {
33140
return next(error);
34141
}

src/controllers/admin/emails/sendCustom.ts

Lines changed: 0 additions & 142 deletions
This file was deleted.

src/controllers/admin/emails/sendTemplate.ts

Lines changed: 0 additions & 38 deletions
This file was deleted.

src/controllers/auth/askResetPassword.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as Sentry from '@sentry/node';
44
import { isNotAuthenticated } from '../../middlewares/authentication';
55
import { validateBody } from '../../middlewares/validation';
66
import { fetchUser, generateResetToken } from '../../operations/user';
7-
import { sendMailsFromTemplate } from '../../services/email';
7+
import { sendPasswordReset } from '../../services/email';
88
import { noContent } from '../../utils/responses';
99
import * as validators from '../../utils/validators';
1010
import logger from '../../utils/logger';
@@ -37,7 +37,7 @@ export default [
3737
// Don't wait for mail to be sent as it could take time
3838
// We suppose here that is will pass. If it is not the case, error is
3939
// reported through Sentry and staff may resend the email manually
40-
sendMailsFromTemplate('passwordreset', [userWithToken]).catch((error) => {
40+
sendPasswordReset(userWithToken).catch((error) => {
4141
logger.error(error);
4242
Sentry.captureException(error, {
4343
user: {

0 commit comments

Comments
 (0)