Skip to content

Commit e21d586

Browse files
committed
feat: update mail sending functionality to support multiple recipient address sources
- Refactored MailSendService and MailSendController to handle multiple recipient address sources, allowing for sending emails to both principal and personnel addresses. - Updated MailSendManyDto to accept an array of recipient address sources, enhancing flexibility in email delivery. - Modified frontend components to support selection of multiple recipient addresses, improving user experience in the mail template modal. - Implemented utility functions for normalizing and collecting recipient emails, ensuring robust email handling.
1 parent 59105cc commit e21d586

7 files changed

Lines changed: 215 additions & 59 deletions

File tree

apps/api/src/management/mail/_dto/mail-send-many.dto.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ApiProperty } from '@nestjs/swagger';
22
import { Types } from 'mongoose';
3-
import { IsArray, IsIn, IsNotEmpty, IsObject, IsOptional, IsString } from 'class-validator';
3+
import { ArrayMinSize, IsArray, IsIn, IsNotEmpty, IsObject, IsOptional, IsString } from 'class-validator';
44

55
export class MailSendManyDto {
66
@ApiProperty({ description: 'Ids des identities destinataires' })
@@ -23,11 +23,14 @@ export class MailSendManyDto {
2323

2424
@ApiProperty({
2525
required: false,
26+
isArray: true,
2627
enum: ['principal', 'personnel'],
2728
description:
28-
'Si renseigné, adresse destinataire lue via le chemin JSON SMTP (e-mail principal ou e-mail personnel). Sinon, politique de mot de passe (emailAttribute).',
29+
'Adresses destinataires via les chemins JSON SMTP (e-mail principal et/ou personnel). Plusieurs valeurs = envoi aux deux adresses lorsqu’elles existent.',
2930
})
3031
@IsOptional()
31-
@IsIn(['principal', 'personnel'])
32-
public recipientAddressSource?: 'principal' | 'personnel';
32+
@IsArray()
33+
@ArrayMinSize(1)
34+
@IsIn(['principal', 'personnel'], { each: true })
35+
public recipientAddressSources?: ('principal' | 'personnel')[];
3336
}

apps/api/src/management/mail/mail-send.controller.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export class MailSendController {
2525
template: body.template,
2626
subject: body.subject,
2727
variables: body.variables,
28-
recipientAddressSource: body.recipientAddressSource,
28+
recipientAddressSources: body.recipientAddressSources,
2929
});
3030
return res.status(HttpStatus.OK).json({ statusCode: HttpStatus.OK, data: result });
3131
}

apps/api/src/management/mail/mail-send.service.ts

Lines changed: 81 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,36 @@ import { MailadmService } from '~/settings/mailadm.service';
77
import { IdentityState } from '~/management/identities/_enums/states.enum';
88
import { isUserSendableMailTemplate } from './mail-templates.service';
99

10+
export type RecipientAddressSource = 'principal' | 'personnel';
11+
12+
function normalizeEmailAddress(raw: unknown): string {
13+
if (raw == null) {
14+
return '';
15+
}
16+
if (Array.isArray(raw)) {
17+
for (const item of raw) {
18+
const email = normalizeEmailAddress(item);
19+
if (email) {
20+
return email;
21+
}
22+
}
23+
return '';
24+
}
25+
const value = String(raw).trim();
26+
return value.includes('@') ? value : '';
27+
}
28+
29+
function collectRecipientEmails(identity: unknown, mailPaths: string[]): string[] {
30+
const emails = new Set<string>();
31+
for (const mailPath of mailPaths) {
32+
const email = normalizeEmailAddress(get(identity, mailPath));
33+
if (email) {
34+
emails.add(email);
35+
}
36+
}
37+
return [...emails];
38+
}
39+
1040
@Injectable()
1141
export class MailSendService {
1242
private readonly logger = new Logger(MailSendService.name);
@@ -18,12 +48,53 @@ export class MailSendService {
1848
private readonly mailadmService: MailadmService,
1949
) {}
2050

51+
private resolveMailPaths(args: {
52+
recipientAddressSources?: RecipientAddressSource[];
53+
principalPath: string;
54+
personnelPath: string;
55+
policyMailAttribute: string;
56+
}): string[] {
57+
const sources = Array.isArray(args.recipientAddressSources)
58+
? [...new Set(args.recipientAddressSources)]
59+
: [];
60+
61+
if (!sources.length) {
62+
if (!args.policyMailAttribute) {
63+
throw new BadRequestException(
64+
'Attribut mail alternatif non configuré (settings.passwordpolicies.emailAttribute)',
65+
);
66+
}
67+
return [args.policyMailAttribute];
68+
}
69+
70+
const mailPaths: string[] = [];
71+
for (const source of sources) {
72+
if (source === 'principal') {
73+
if (!args.principalPath) {
74+
throw new BadRequestException(
75+
"Chemin JSON « e-mail principal » non configuré (paramètres → Serveur SMTP → Chemin JSON de l'e-mail principal).",
76+
);
77+
}
78+
mailPaths.push(args.principalPath);
79+
} else if (source === 'personnel') {
80+
if (!args.personnelPath) {
81+
throw new BadRequestException(
82+
"Chemin JSON « e-mail personnel » non configuré (paramètres → Serveur SMTP → Chemin JSON de l'e-mail personnel).",
83+
);
84+
}
85+
mailPaths.push(args.personnelPath);
86+
}
87+
}
88+
89+
return mailPaths;
90+
}
91+
2192
public async sendTemplateToIdentities(args: {
2293
ids: string[];
2394
template: string;
2495
subject: string;
2596
variables?: Record<string, string>;
26-
recipientAddressSource?: 'principal' | 'personnel';
97+
recipientAddressSources?: RecipientAddressSource[];
2798
}): Promise<{ sent: number; skipped: number }> {
2899
const template = String(args.template || '').trim();
29100
if (!template) {
@@ -50,30 +121,12 @@ export class MailSendService {
50121
const policies: any = await this.passwdadmService.getPolicies();
51122
const policyMailAttribute = String(policies?.emailAttribute || '');
52123

53-
const source = args.recipientAddressSource;
54-
let mailPath: string;
55-
if (source === 'principal') {
56-
mailPath = principalPath;
57-
if (!mailPath) {
58-
throw new BadRequestException(
59-
"Chemin JSON « e-mail principal » non configuré (paramètres → Serveur SMTP → Chemin JSON de l'e-mail principal).",
60-
);
61-
}
62-
} else if (source === 'personnel') {
63-
mailPath = personnelPath;
64-
if (!mailPath) {
65-
throw new BadRequestException(
66-
"Chemin JSON « e-mail personnel » non configuré (paramètres → Serveur SMTP → Chemin JSON de l'e-mail personnel).",
67-
);
68-
}
69-
} else {
70-
mailPath = policyMailAttribute;
71-
if (!mailPath) {
72-
throw new BadRequestException(
73-
'Attribut mail alternatif non configuré (settings.passwordpolicies.emailAttribute)',
74-
);
75-
}
76-
}
124+
const mailPaths = this.resolveMailPaths({
125+
recipientAddressSources: args.recipientAddressSources,
126+
principalPath,
127+
personnelPath,
128+
policyMailAttribute,
129+
});
77130

78131
const identities = await this.identities.model.find({ _id: { $in: args.ids }, state: IdentityState.SYNCED }).lean();
79132
if (!identities?.length) {
@@ -84,15 +137,15 @@ export class MailSendService {
84137
let skipped = 0;
85138

86139
for (const identity of identities) {
87-
const to = get(identity as any, mailPath) as string;
88-
if (!to) {
140+
const recipients = collectRecipientEmails(identity, mailPaths);
141+
if (!recipients.length) {
89142
skipped++;
90143
continue;
91144
}
92145

93146
try {
94147
await this.mailer.sendMail({
95-
to,
148+
to: recipients.length === 1 ? recipients[0] : recipients,
96149
subject,
97150
template,
98151
context: {

apps/web/src/components/pages/identities/modals/mail-template.vue

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -73,13 +73,15 @@ q-dialog(
7373
strong Paramètres → Serveur SMTP
7474
| .
7575
q-select.q-mt-md(
76-
v-if="!isInternalTemplate && recipientSourceOptions.length > 1"
77-
v-model="recipientAddressSource"
76+
v-if="!isInternalTemplate && recipientSourceOptions.length > 0"
77+
v-model="recipientAddressSources"
7878
:options="recipientSourceOptions"
79-
label="Adresse du destinataire"
80-
hint="Chemin JSON défini dans Paramètres → Serveur SMTP"
79+
label="Adresse(s) du destinataire"
80+
hint="Une ou plusieurs adresses (principal et/ou personnel)"
8181
outlined
8282
dense
83+
multiple
84+
use-chips
8385
emit-value
8486
map-options
8587
color="teal-7"
@@ -252,13 +254,15 @@ q-dialog(
252254
strong Paramètres → Serveur SMTP
253255
| .
254256
q-select.q-mt-md(
255-
v-if="!isInternalTemplate && recipientSourceOptions.length > 1"
256-
v-model="recipientAddressSource"
257+
v-if="!isInternalTemplate && recipientSourceOptions.length > 0"
258+
v-model="recipientAddressSources"
257259
:options="recipientSourceOptions"
258-
label="Adresse du destinataire"
259-
hint="Chemin JSON défini dans Paramètres → Serveur SMTP"
260+
label="Adresse(s) du destinataire"
261+
hint="Une ou plusieurs adresses (principal et/ou personnel)"
260262
outlined
261263
dense
264+
multiple
265+
use-chips
262266
emit-value
263267
map-options
264268
color="teal-7"
@@ -529,7 +533,7 @@ const availableVariables = ref<{ key: string; label?: string; description?: stri
529533
530534
const mailPaths = ref<{ personnel: string; principal: string }>({ personnel: '', principal: '' })
531535
const mailPathsReady = ref(false)
532-
const recipientAddressSource = ref<'principal' | 'personnel' | null>(null)
536+
const recipientAddressSources = ref<('principal' | 'personnel')[]>([])
533537
534538
const isInternalTemplate = computed(() => {
535539
const name = String(templateName.value || '').trim()
@@ -561,7 +565,7 @@ const canSendMailTemplate = computed(
561565
!isInternalTemplate.value &&
562566
mailPathsReady.value &&
563567
String(mailSubject.value || '').trim().length > 0 &&
564-
(recipientAddressSource.value === 'principal' || recipientAddressSource.value === 'personnel'),
568+
recipientAddressSources.value.length > 0,
565569
)
566570
567571
const sendButtonTitle = computed(() => {
@@ -580,7 +584,7 @@ const sendButtonTitle = computed(() => {
580584
if (recipientSourceOptions.value.length === 0) {
581585
return 'Configurez au moins un chemin JSON (e-mail personnel ou principal) dans Paramètres → Serveur SMTP.'
582586
}
583-
return "Choisissez l'adresse du destinataire (e-mail personnel ou principal)."
587+
return "Sélectionnez au moins une adresse destinataire (e-mail personnel et/ou principal)."
584588
})
585589
586590
const addVar = () => {
@@ -626,20 +630,17 @@ async function fetchMailPathsConfig() {
626630
personnel: String(d.recipientJsonPathEmailPersonnel || '').trim(),
627631
principal: String(d.recipientJsonPathEmailPrincipal || '').trim(),
628632
}
629-
const per = mailPaths.value.personnel
630-
const prin = mailPaths.value.principal
631-
if (prin && per) {
632-
recipientAddressSource.value = 'principal'
633-
} else if (prin) {
634-
recipientAddressSource.value = 'principal'
635-
} else if (per) {
636-
recipientAddressSource.value = 'personnel'
637-
} else {
638-
recipientAddressSource.value = null
633+
const sources: ('principal' | 'personnel')[] = []
634+
if (mailPaths.value.principal) {
635+
sources.push('principal')
639636
}
637+
if (mailPaths.value.personnel) {
638+
sources.push('personnel')
639+
}
640+
recipientAddressSources.value = sources
640641
} catch {
641642
mailPaths.value = { personnel: '', principal: '' }
642-
recipientAddressSource.value = null
643+
recipientAddressSources.value = []
643644
} finally {
644645
mailPathsReady.value = true
645646
}
@@ -742,7 +743,7 @@ const syncIdentities = () => {
742743
template: templateName.value,
743744
subject: String(mailSubject.value || '').trim(),
744745
variables: variablesToSend.value,
745-
recipientAddressSource: recipientAddressSource.value,
746+
recipientAddressSources: recipientAddressSources.value,
746747
})
747748
}
748749

apps/web/src/pages/identities/table.vue

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -552,7 +552,12 @@ export default defineNuxtComponent({
552552
553553
async sendTemplateMailToIdentities(
554554
identities,
555-
data: { template?: string; subject?: string; variables?: Record<string, string>; recipientAddressSource?: string },
555+
data: {
556+
template?: string
557+
subject?: string
558+
variables?: Record<string, string>
559+
recipientAddressSources?: ('principal' | 'personnel')[]
560+
},
556561
) {
557562
const ids = this.bulkIdsFromIdentities(identities)
558563
try {
@@ -562,7 +567,7 @@ export default defineNuxtComponent({
562567
template: data?.template,
563568
subject: data?.subject,
564569
variables: data?.variables,
565-
...(data?.recipientAddressSource ? { recipientAddressSource: data.recipientAddressSource } : {}),
570+
...(data?.recipientAddressSources?.length ? { recipientAddressSources: data.recipientAddressSources } : {}),
566571
},
567572
})
568573
const payload = (result as { _data?: { data?: { sent?: number; skipped?: number } } })._data?.data

apps/web/src/pages/identities/table/[_id]/index.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -440,7 +440,7 @@ export default defineNuxtComponent({
440440
template: data?.template,
441441
subject: data?.subject,
442442
variables: data?.variables,
443-
...(data?.recipientAddressSource ? { recipientAddressSource: data.recipientAddressSource } : {}),
443+
...(data?.recipientAddressSources?.length ? { recipientAddressSources: data.recipientAddressSources } : {}),
444444
},
445445
})
446446
this.$q.notify({

0 commit comments

Comments
 (0)