File

src/module/mail/mail.service.ts

Index

Properties

Properties

options
options: IMailingSettings
Type : IMailingSettings
transport
transport: Mail
Type : Mail
user
user: User
Type : User
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 ""