From 6e69dc8d2b3f6d1df0a657e6e9b1c98064c08b1b Mon Sep 17 00:00:00 2001 From: evenegas Date: Sat, 15 Mar 2025 12:02:41 -0600 Subject: [PATCH] =?UTF-8?q?Se=20agreg=C3=B3=20la=20contrase=C3=B1a=20de=20?= =?UTF-8?q?acceso?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/excel/excel.service.ts | 5 +- src/mail/mail.controller.ts | 54 +++++++++-- src/mail/mail.module.ts | 2 + src/mail/mail.service.spec.ts | 90 +++++++++++++++++ src/mail/mail.service.ts | 129 ++----------------------- src/main.ts | 2 +- src/typeorm/votacionesPrueba.entity.ts | 44 +++++---- 7 files changed, 174 insertions(+), 152 deletions(-) create mode 100644 src/mail/mail.service.spec.ts diff --git a/src/excel/excel.service.ts b/src/excel/excel.service.ts index 9dde3ff..6f4b2da 100644 --- a/src/excel/excel.service.ts +++ b/src/excel/excel.service.ts @@ -7,10 +7,7 @@ export class ExcelService { async readEmailsFromExcel(file: Express.Multer.File) { - if (!file) { - - throw new Error('Archivo Excel no encontrado'); - } + const workbook = new ExcelJS.Workbook(); diff --git a/src/mail/mail.controller.ts b/src/mail/mail.controller.ts index 7fe899a..866a308 100644 --- a/src/mail/mail.controller.ts +++ b/src/mail/mail.controller.ts @@ -3,12 +3,16 @@ import { Post, Body, UploadedFile, - UseInterceptors, + Headers, + UnauthorizedException, } from '@nestjs/common'; import { MailService } from './mail.service'; import { ExcelService } from 'src/excel/excel.service'; import { ApiTags } from '@nestjs/swagger'; import { ApiSendMail, ApiSendMailExcel} from './mail.decorators'; +import { Repository } from 'typeorm'; +import { Sistema } from 'src/typeorm/votacionesPrueba.entity'; +import { InjectRepository } from '@nestjs/typeorm'; @ApiTags('Mail') // Grupo de endpoints @@ -16,30 +20,66 @@ import { ApiSendMail, ApiSendMailExcel} from './mail.decorators'; export class MailController { constructor( private readonly mailService: MailService, - private readonly excelService: ExcelService + private readonly excelService: ExcelService, + @InjectRepository(Sistema) private readonly sistemaRepository: Repository, + ) {} // Endpoint para enviar correos a través de una solicitud POST @Post('send') @ApiSendMail() - async sendMail(@Body() body: { to: string; subject: string; text: string; fecha_recibido: Date }) { - const { to, subject, text, fecha_recibido } = body; + async sendMail( + @Headers() headers:{password:string}, + @Body() body: { to: string; subject: string; text: string; fecha_recibido: Date }) { - const result = await this.mailService.sendMail(to, subject, text, fecha_recibido); + + if (!headers.password) { + throw new UnauthorizedException('Header "Password" es requerido'); + } + + const sistem = await this.sistemaRepository.findOne({ where: { password: headers.password } }); + + if (!sistem) { + throw new UnauthorizedException('Password incorrecto'); + } + + + const { to, subject, text, fecha_recibido } = body; + + const result = await this.mailService.sendMail(to, subject, text, fecha_recibido,sistem); return result; + + + + } @Post('send-excel') @ApiSendMailExcel() - async sendMailExcel(@UploadedFile() file: Express.Multer.File) { + async sendMailExcel(@Headers() headers:{password:string},@Body() file: Express.Multer.File) { + + if (!headers.password) { + throw new UnauthorizedException('Header "Password" es requerido'); + } + + const sistem = await this.sistemaRepository.findOne({ where: { password: headers.password } }); + + if (!sistem) { + throw new UnauthorizedException('Password incorrecto'); + } + + if (!file) { + + throw new Error('Archivo Excel no encontrado'); + } const emails=this.excelService.readEmailsFromExcel(file); let fechaActual: Date = new Date(); for (const email of await emails) { try { - await this.mailService.sendMail(email, "subject", "text", fechaActual); + await this.mailService.sendMail(email, "subject", "text", fechaActual,sistem); console.log(`📧 Enviado a: ${email}`); } catch (error) { console.error(`❌ Error enviando a ${email}:`, error.message); diff --git a/src/mail/mail.module.ts b/src/mail/mail.module.ts index 766c3ea..8208010 100644 --- a/src/mail/mail.module.ts +++ b/src/mail/mail.module.ts @@ -6,6 +6,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { Status, Correo, + Sistema, }from 'src/typeorm/votacionesPrueba.entity' @Module({ @@ -15,6 +16,7 @@ import { TypeOrmModule.forFeature([ Status, Correo, + Sistema, ]), ], controllers: [MailController], diff --git a/src/mail/mail.service.spec.ts b/src/mail/mail.service.spec.ts new file mode 100644 index 0000000..b31f369 --- /dev/null +++ b/src/mail/mail.service.spec.ts @@ -0,0 +1,90 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { MailService } from './mail.service'; +import { getRepositoryToken } from '@nestjs/typeorm'; +import { Repository } from 'typeorm'; +import { Correo, Sistema, Status } from '../typeorm/votacionesPrueba.entity'; // Ajusta si es necesario + +describe('MailService', () => { + let service: MailService; + let correoRepository: Repository; + let statusRepository: Repository; + let sistemaRepository: Repository; + + const mockCorreoRepository = { + create: jest.fn(), + save: jest.fn(), + }; + + const mockStatusRepository = { + findOne: jest.fn(), + create: jest.fn(), + save: jest.fn(), + }; + + const mockSistemaRepository = { + findOne: jest.fn(), + create: jest.fn(), + save: jest.fn(), + }; + + const mockTransporter = { + sendMail: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + MailService, + { provide: getRepositoryToken(Correo), useValue: mockCorreoRepository }, + { provide: getRepositoryToken(Status), useValue: mockStatusRepository }, + ], + }).compile(); + + service = module.get(MailService); + correoRepository = module.get>(getRepositoryToken(Correo)); + statusRepository = module.get>(getRepositoryToken(Status)); + sistemaRepository = module.get>(getRepositoryToken(Sistema)); + + + service['transporter'] = mockTransporter; + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('debe enviar un correo exitosamente y guardar en la base de datos', async () => { + const to = 'test@example.com'; + const subject = 'Test Email'; + const text = 'Este es un correo de prueba'; + const fecha_recibido = new Date(); + const sistema = mockSistemaRepository.findOne.mockResolvedValue({id:1}); + + mockTransporter.sendMail.mockResolvedValue({ accepted: [to] }); + + mockStatusRepository.findOne.mockResolvedValue({ id: 1, status: 'Enviado' }); + + mockCorreoRepository.create.mockReturnValue({ id: 1 }); + + const result = await service.sendMail(to, subject, text, fecha_recibido,sistema); + + expect(result).toBe('Enviado'); + + expect(mockTransporter.sendMail).toHaveBeenCalledWith({ + from: process.env.USER_GMAIL, + to, + subject, + text, + }); + + + expect(mockStatusRepository.findOne).toHaveBeenCalledWith({ + where: { status: 'Enviado' }, + }); + + expect(mockCorreoRepository.create).toHaveBeenCalled(); + expect(mockCorreoRepository.save).toHaveBeenCalled(); + }); + + +}); diff --git a/src/mail/mail.service.ts b/src/mail/mail.service.ts index 44aeb15..9e1b6f9 100644 --- a/src/mail/mail.service.ts +++ b/src/mail/mail.service.ts @@ -3,7 +3,7 @@ import * as nodemailer from 'nodemailer'; import * as dotenv from 'dotenv'; import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; -import { Correo, Status } from 'src/typeorm/votacionesPrueba.entity'; +import { Correo, Status } from '../typeorm/votacionesPrueba.entity'; dotenv.config(); @@ -22,8 +22,8 @@ export class MailService { this.transporter = nodemailer.createTransport({ service: 'gmail', auth: { - user: process.env.USER_GMAIL, // tu dirección de correo de Gmail - pass: process.env.PASS_GMAIL, // contraseña de aplicación generada en Google + user: process.env.USER_GMAIL, + pass: process.env.PASS_GMAIL, }, }); @@ -40,33 +40,9 @@ export class MailService { } // Función para generar el reporte de correos enviados y fallidos - private generateReport( - sentEmails: string[], - failedEmails: { email: string; error: any }[], - totalTime: number, - ) { - console.log('--- Reporte de envío de correos ---'); - console.log(`Tiempo total de envío: ${totalTime} segundos`); + - console.log('\nCorreos enviados con éxito:'); - sentEmails.forEach((email) => console.log(email)); - - console.log('\nCorreos fallidos:'); - failedEmails.forEach((failed) => - console.log(`Correo: ${failed.email} - Error: ${failed.error.message}`), - ); - - console.log(`\nTotal de correos enviados: ${sentEmails.length}`); - console.log(`Total de correos fallidos: ${failedEmails.length}`); - - if (failedEmails.length > 0) { - console.log( - '\nAlgunos correos fallaron. Inténtalos de nuevo de manera manual.', - ); - } - } - - async sendMail(to: string, subject: string, text: string, fecha_recibido: Date) { + async sendMail(to: string, subject: string, text: string, fecha_recibido: Date, sistema) { const mailOptions = { from: process.env.USER_GMAIL, to, @@ -88,7 +64,7 @@ export class MailService { const info= this.correoRepository.create({ id_status: status, - + id_sistema: sistema, fecha_recibido: fecha_recibido, fecha_enviado: new Date(), destinatario: to, @@ -102,96 +78,9 @@ export class MailService { - return + return (statusTexto); } - async sendBulkMailsOld(emails: string[], subject: string, text: string) { - const batchSize = 100; // Número de correos por lote - for (let i = 0; i < emails.length; i += batchSize) { - const batch = emails.slice(i, i + batchSize); - const promises = batch.map((email) => - this.transporter.sendMail({ - from: process.env.USER_GMAIL, - to: email, - subject, - text, - }), - ); - - // Esperar que se envíen todos los correos del lote - await Promise.all(promises); - - // Delay entre lotes para no sobrecargar el servidor de Gmail - await new Promise((resolve) => setTimeout(resolve, 2000)); // 2 segundos de pausa - } - } - - async sendBulkMails(emails: string[], subject: string, text: string) { - const batchSize = 100; - const sentEmails: string[] = []; - const failedEmails: { email: string; error: any }[] = []; - const startTime = Date.now(); // Inicia el temporizador - - for (let i = 0; i < emails.length; i += batchSize) { - const batch = emails.slice(i, i + batchSize); - const promises = batch.map(async (email) => { - try { - await this.transporter.sendMail({ - from: process.env.USER_GMAIL, - to: email, - subject, - text, - }); - sentEmails.push(email); // Agregar a lista de enviados exitosamente - } catch (error) { - - failedEmails.push({ email, error }); // Agregar a lista de fallos - } - }); - - // Espera a que todos los correos del lote se envíen - await Promise.all(promises); - - // Delay para evitar saturar el servidor de correo - await new Promise((resolve) => setTimeout(resolve, 2000)); - } - - const totalTime = (Date.now() - startTime) / 1000; // Tiempo total en segundos - - // Generar el reporte final - this.generateReport(sentEmails, failedEmails, totalTime); - } - - async retryFailedEmails( - failedEmails: { email: string; error: any }[], - subject: string, - text: string, - ) { - const retrySuccess: string[] = []; - const retryFailed: { email: string; error: any }[] = []; - - for (const failed of failedEmails) { - try { - await this.transporter.sendMail({ - from: process.env.USER_GMAIL, - to: failed.email, - subject, - text, - }); - retrySuccess.push(failed.email); // Reintento exitoso - } catch (error) { - - retryFailed.push({ email: failed.email, error }); // Reintento fallido - } - } - - console.log('--- Reintento de correos fallidos ---'); - console.log('\nCorreos reenviados con éxito:'); - retrySuccess.forEach((email) => console.log(email)); - - console.log('\nCorreos que fallaron nuevamente:'); - retryFailed.forEach((failed) => - console.log(`Correo: ${failed.email} - Error: ${failed.error.message}`), - ); - } + + } diff --git a/src/main.ts b/src/main.ts index e91e9cf..77262f1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,7 +7,7 @@ async function bootstrap() { const app = await NestFactory.create(AppModule); const config = new DocumentBuilder() - .setTitle('Documentacion de la api de votaciones v2') + .setTitle('Documentacion de la api de votaciones_mails v2') .setDescription( 'El proyecto ha sido refactorizado a una version mejorada con nestjs', ) diff --git a/src/typeorm/votacionesPrueba.entity.ts b/src/typeorm/votacionesPrueba.entity.ts index 82d4b0e..71bd67d 100644 --- a/src/typeorm/votacionesPrueba.entity.ts +++ b/src/typeorm/votacionesPrueba.entity.ts @@ -1,15 +1,4 @@ -import { Entity, PrimaryGeneratedColumn, Column, ManyToOne } from 'typeorm'; - -@Entity() -export class Status { - @PrimaryGeneratedColumn() - id_status: number; - - @Column({ type: 'varchar', length: 50, unique:true}) - status: string; - - correos: Correo[]; -} +import { Entity, PrimaryGeneratedColumn, Column, OneToMany, ManyToOne, JoinColumn } from 'typeorm'; @Entity() export class Sistema { @@ -17,31 +6,46 @@ export class Sistema { id_sistema: number; @Column({ type: 'varchar', length: 50 }) - refresh_token: string; + password: string; - @Column({ type: 'varchar', length: 50}) - clave: string; - - @Column({ type: 'varchar', length: 50}) + @Column({ type: 'varchar', length: 50 }) nombre_sistema: string; + @OneToMany(() => Correo, (correo) => correo.id_sistema) correos: Correo[]; } +@Entity() +export class Status { + @PrimaryGeneratedColumn() + id_status: number; + + @Column({ type: 'varchar', length: 50, unique: true }) + status: string; + + @OneToMany(() => Correo, (correo) => correo.id_status) + correos: Correo[]; +} + + + @Entity() export class Correo { @PrimaryGeneratedColumn() id_correo: number; @ManyToOne(() => Status, (status) => status.correos) + @JoinColumn({ name: 'id_status' }) id_status: Status; - + @ManyToOne(() => Sistema, (sistema) => sistema.correos) + @JoinColumn({ name: 'id_sistema' }) + id_sistema: Sistema; - @Column({ type: 'datetime'}) + @Column({ type: 'datetime' }) fecha_recibido: Date; - @Column({ type: 'datetime'}) + @Column({ type: 'datetime' }) fecha_enviado: Date; @Column({ type: 'varchar', length: 50 })