File

src/module/mail/mail.service.ts

Index

Methods

Constructor

constructor(userService: UserService, settingsService: SettingsService, templateService: TemplateService)
Parameters :
Name Type Optional
userService UserService No
settingsService SettingsService No
templateService TemplateService No

Methods

Private createSMTPTransport
createSMTPTransport(options: IMailingSettings)
Parameters :
Name Type Optional Description
options IMailingSettings No

Options to create the transport with.

Returns : Mail

A nodemail SMTPTransport instance created with the given options.

Private generateMailingStatus
generateMailingStatus(promiseResults: PromiseSettledResult[])

Converts the given promise results into a MailingStatus object extracting the important information:

  • Information about all failed ones.
  • Amount of successfully sent ones.
Parameters :
Name Type Optional Description
promiseResults PromiseSettledResult<SentMessageInfo>[] No

All promise results from settled promises sending mails.

Returns : MailingStatus

MailingStatus according to the responses.

Private getTextOfMail
getTextOfMail(user: User)
Parameters :
Name Type Optional Description
user User No

User to get the mail text for.

Returns : string

The mail template filled with the data of the given user

Private isValidEmail
isValidEmail(email: string)
Parameters :
Name Type Optional Description
email string No

String to check.

Returns : boolean

Is the string a valid email address?

Async mailCredentials
mailCredentials()

Sends a mail with the credentials to all users.

The mail is only sent to users which are not the 'admin' user and which did not already changed their initial password (ie still have a temporary password).

Data containing information about the amount of successfully send mails and information about failed ones.

Async mailSingleCredentials
mailSingleCredentials(userId: string)

Sends a mail with the credentials to the user with the given ID.

The mail is only sent to the user if he/she has not already changed their initial password.

Parameters :
Name Type Optional
userId string No

Data containing information about the amount of successfully send mails (1) and information on failure.

Private Async sendMail
sendMail(undefined: SendMailParams)

Tries to send a mail with the credentials of the given user.

If the mail was sent successfully a SentMessageInfo is returned. If it fails an error is thrown.

Parameters :
Name Type Optional
SendMailParams No
Returns : Promise<SentMessageInfo>

Info about the sent message.

import { Injectable, InternalServerErrorException } from '@nestjs/common';
import nodemailer from 'nodemailer';
import Mail from 'nodemailer/lib/mailer';
import { SentMessageInfo } from 'nodemailer/lib/smtp-transport';
import { FailedMail, MailingStatus } from 'shared/model/Mail';
import { IMailingSettings } from 'shared/model/Settings';
import { getNameOfEntity } from 'shared/util/helpers';
import { User } from '../../database/entities/user.entity';
import { VALID_EMAIL_REGEX } from '../../helpers/validators/nodemailer.validator';
import { SettingsService } from '../settings/settings.service';
import { TemplateService } from '../template/template.service';
import { UserService } from '../user/user.service';

class MailingError {
    constructor(
        readonly userId: string,
        readonly message: string,
        readonly err: unknown
    ) {}
}

interface SendMailParams {
    user: User;
    transport: Mail;
    options: IMailingSettings;
}

@Injectable()
export class MailService {
    constructor(
        private readonly userService: UserService,
        private readonly settingsService: SettingsService,
        private readonly templateService: TemplateService
    ) {}

    /**
     * Sends a mail with the credentials to all users.
     *
     * The mail is only sent to users which are not the 'admin' user and which did not already changed their initial password (ie still have a temporary password).
     *
     * @returns Data containing information about the amount of successfully send mails and information about failed ones.
     */
    async mailCredentials(): Promise<MailingStatus> {
        const options = await this.settingsService.getMailingOptions();

        if (!options) {
            throw new InternalServerErrorException('MISSING_MAIL_SETTINGS');
        }

        const transport = this.createSMTPTransport(options);
        const usersToMail = (await this.userService.findAll()).filter(
            (u) => u.username !== 'admin' && !!u.temporaryPassword
        );
        const mails: Promise<SentMessageInfo>[] = [];
        const failedMails: FailedMail[] = [];

        for (const user of usersToMail) {
            if (this.isValidEmail(user.email)) {
                mails.push(this.sendMail({ user, transport, options }));
            } else {
                failedMails.push({
                    userId: user.id,
                    reason: 'INVALID_EMAIL_ADDRESS',
                });
            }
        }

        const status = this.generateMailingStatus(await Promise.allSettled(mails));
        transport.close();

        return status;
    }

    /**
     * Sends a mail with the credentials to the user with the given ID.
     *
     * The mail is only sent to the user if he/she has not already changed their initial password.
     *
     * @returns Data containing information about the amount of successfully send mails (1) and information on failure.
     */
    async mailSingleCredentials(userId: string): Promise<MailingStatus> {
        const options = await this.settingsService.getMailingOptions();

        if (!options) {
            throw new InternalServerErrorException('MISSING_MAIL_SETTINGS');
        }

        const transport = this.createSMTPTransport(options);
        const user = await this.userService.findById(userId);

        if (!user.temporaryPassword) {
            return {
                successFullSend: 0,
                failedMailsInfo: [{ userId: user.id, reason: 'NO_TEMP_PWD_ON_USER' }],
            };
        }

        if (!this.isValidEmail(user.email)) {
            return {
                successFullSend: 0,
                failedMailsInfo: [{ userId: user.id, reason: 'INVALID_EMAIL_ADDRESS' }],
            };
        }

        const status = await Promise.allSettled([this.sendMail({ user, transport, options })]);
        transport.close();

        return this.generateMailingStatus(status);
    }

    /**
     * Converts the given promise results into a `MailingStatus` object extracting the important information:
     *
     * - Information about all failed ones.
     * - Amount of successfully sent ones.
     *
     * @param promiseResults All promise results from settled promises sending mails.
     * @returns `MailingStatus` according to the responses.
     */
    private generateMailingStatus(
        promiseResults: PromiseSettledResult<SentMessageInfo>[]
    ): MailingStatus {
        const status: MailingStatus = {
            failedMailsInfo: [],
            successFullSend: 0,
        };

        for (const mail of promiseResults) {
            if (mail.status === 'fulfilled') {
                status.successFullSend += 1;
            } else {
                if (mail.reason instanceof MailingError) {
                    status.failedMailsInfo.push({
                        userId: mail.reason.userId,
                        reason: `${mail.reason.message}:\n${JSON.stringify(
                            mail.reason.err,
                            null,
                            2
                        )}`,
                    });
                } else {
                    status.failedMailsInfo.push({
                        userId: 'UNKNOWN',
                        reason: 'UNKNOWN_ERROR',
                    });
                }
            }
        }

        return status;
    }

    /**
     * Tries to send a mail with the credentials of the given user.
     *
     * If the mail was sent successfully a `SentMessageInfo` is returned. If it fails an error is thrown.
     *
     * @param user User to send the mail to
     * @param options MailingConfiguration
     * @param transport Transport to use.
     *
     * @returns Info about the sent message.
     * @throws `MailingError` - If the mail could not be successfully send.
     */
    private async sendMail({ user, options, transport }: SendMailParams): Promise<SentMessageInfo> {
        try {
            return await transport.sendMail({
                from: options.from,
                to: user.email,
                subject: options.subject,
                text: this.getTextOfMail(user),
            });
        } catch (err) {
            throw new MailingError(user.id, 'SEND_MAIL_FAILED', err);
        }
    }

    /**
     * @param user User to get the mail text for.
     * @returns The mail template filled with the data of the given user
     */
    private getTextOfMail(user: User): string {
        const template = this.templateService.getMailTemplate();
        return template({
            name: getNameOfEntity(user, { firstNameFirst: true }),
            username: user.username,
            password: user.temporaryPassword ?? 'NO_TMP_PASSWORD',
        });
    }

    /**
     * @param options Options to create the transport with.
     * @returns A nodemail SMTPTransport instance created with the given options.
     */
    private createSMTPTransport(options: IMailingSettings): Mail {
        return nodemailer.createTransport({
            host: options.host,
            port: options.port,
            auth: { user: options.auth.user, pass: options.auth.pass },
            logger: true,
            debug: true,
        });
    }

    /**
     * @param email String to check.
     * @returns Is the string a valid email address?
     */
    private isValidEmail(email: string): boolean {
        return VALID_EMAIL_REGEX.test(email);
    }

    // private isOAuth2(auth: SMTPConnection.AuthenticationType): auth is AuthenticationTypeOAuth2 {
    //   return auth.type === 'oauth2' || auth.type === 'OAuth2' || auth.type === 'OAUTH2';
    // }

    // private isBasicAuth(auth: SMTPConnection.AuthenticationType): auth is AuthenticationTypeLogin {
    //   return !auth.type || auth.type === 'login' || auth.type === 'Login' || auth.type === 'LOGIN';
    // }
}

results matching ""

    No results matching ""