mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 20:26:42 +02:00
chore: archive finance, mail, moodlit apps and rename voxel-lava
- Move finance, mail, moodlit to apps-archived for later development - Rename games/voxel-lava to games/voxelava 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c3c272abc9
commit
ace7fa8f7f
427 changed files with 0 additions and 0 deletions
|
|
@ -0,0 +1,108 @@
|
|||
import {
|
||||
Controller,
|
||||
Get,
|
||||
Post,
|
||||
Patch,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
UseGuards,
|
||||
ParseUUIDPipe,
|
||||
} from '@nestjs/common';
|
||||
import { JwtAuthGuard, CurrentUser, CurrentUserData } from '@manacore/shared-nestjs-auth';
|
||||
import { ComposeService } from './compose.service';
|
||||
import { CreateDraftDto, UpdateDraftDto, SendEmailDto, DraftQueryDto } from './dto/compose.dto';
|
||||
|
||||
@Controller()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
export class ComposeController {
|
||||
constructor(private readonly composeService: ComposeService) {}
|
||||
|
||||
// ==================== Drafts ====================
|
||||
|
||||
@Get('drafts')
|
||||
async findAllDrafts(@CurrentUser() user: CurrentUserData, @Query() query: DraftQueryDto) {
|
||||
const drafts = await this.composeService.findDraftsByUserId(user.userId, query);
|
||||
const total = await this.composeService.countDrafts(user.userId, query.accountId);
|
||||
return { drafts, total };
|
||||
}
|
||||
|
||||
@Get('drafts/:id')
|
||||
async findDraft(@CurrentUser() user: CurrentUserData, @Param('id', ParseUUIDPipe) id: string) {
|
||||
const draft = await this.composeService.findDraftById(id, user.userId);
|
||||
if (!draft) {
|
||||
return { draft: null };
|
||||
}
|
||||
return { draft };
|
||||
}
|
||||
|
||||
@Post('drafts')
|
||||
async createDraft(@CurrentUser() user: CurrentUserData, @Body() dto: CreateDraftDto) {
|
||||
const draft = await this.composeService.createDraft({
|
||||
...dto,
|
||||
userId: user.userId,
|
||||
scheduledAt: dto.scheduledAt ? new Date(dto.scheduledAt) : null,
|
||||
});
|
||||
return { draft };
|
||||
}
|
||||
|
||||
@Patch('drafts/:id')
|
||||
async updateDraft(
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
@Param('id', ParseUUIDPipe) id: string,
|
||||
@Body() dto: UpdateDraftDto
|
||||
) {
|
||||
const draft = await this.composeService.updateDraft(id, user.userId, {
|
||||
...dto,
|
||||
scheduledAt: dto.scheduledAt ? new Date(dto.scheduledAt) : undefined,
|
||||
});
|
||||
return { draft };
|
||||
}
|
||||
|
||||
@Delete('drafts/:id')
|
||||
async deleteDraft(@CurrentUser() user: CurrentUserData, @Param('id', ParseUUIDPipe) id: string) {
|
||||
await this.composeService.deleteDraft(id, user.userId);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@Post('drafts/:id/send')
|
||||
async sendDraft(@CurrentUser() user: CurrentUserData, @Param('id', ParseUUIDPipe) id: string) {
|
||||
const result = await this.composeService.sendDraft(id, user.userId);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ==================== Direct Send ====================
|
||||
|
||||
@Post('send')
|
||||
async sendEmail(@CurrentUser() user: CurrentUserData, @Body() dto: SendEmailDto) {
|
||||
const result = await this.composeService.sendEmail(user.userId, dto);
|
||||
return result;
|
||||
}
|
||||
|
||||
// ==================== Reply/Forward ====================
|
||||
|
||||
@Post('emails/:id/reply')
|
||||
async createReply(@CurrentUser() user: CurrentUserData, @Param('id', ParseUUIDPipe) id: string) {
|
||||
const draft = await this.composeService.createReplyDraft(user.userId, id, 'reply');
|
||||
return { draft };
|
||||
}
|
||||
|
||||
@Post('emails/:id/reply-all')
|
||||
async createReplyAll(
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
@Param('id', ParseUUIDPipe) id: string
|
||||
) {
|
||||
const draft = await this.composeService.createReplyDraft(user.userId, id, 'reply-all');
|
||||
return { draft };
|
||||
}
|
||||
|
||||
@Post('emails/:id/forward')
|
||||
async createForward(
|
||||
@CurrentUser() user: CurrentUserData,
|
||||
@Param('id', ParseUUIDPipe) id: string
|
||||
) {
|
||||
const draft = await this.composeService.createReplyDraft(user.userId, id, 'forward');
|
||||
return { draft };
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
import { ComposeController } from './compose.controller';
|
||||
import { ComposeService } from './compose.service';
|
||||
import { AccountModule } from '../account/account.module';
|
||||
import { EmailModule } from '../email/email.module';
|
||||
|
||||
@Module({
|
||||
imports: [AccountModule, EmailModule],
|
||||
controllers: [ComposeController],
|
||||
providers: [ComposeService],
|
||||
exports: [ComposeService],
|
||||
})
|
||||
export class ComposeModule {}
|
||||
363
apps-archived/mail/apps/backend/src/compose/compose.service.ts
Normal file
363
apps-archived/mail/apps/backend/src/compose/compose.service.ts
Normal file
|
|
@ -0,0 +1,363 @@
|
|||
import { Injectable, Inject, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||
import { eq, and, desc, sql } from 'drizzle-orm';
|
||||
import { DATABASE_CONNECTION } from '../db/database.module';
|
||||
import { type Database } from '../db/connection';
|
||||
import { drafts, type Draft, type NewDraft, emailAccounts, type EmailAddress } from '../db/schema';
|
||||
import { AccountService } from '../account/account.service';
|
||||
import { EmailService } from '../email/email.service';
|
||||
import * as nodemailer from 'nodemailer';
|
||||
|
||||
export interface DraftFilters {
|
||||
accountId?: string;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class ComposeService {
|
||||
constructor(
|
||||
@Inject(DATABASE_CONNECTION) private db: Database,
|
||||
private accountService: AccountService,
|
||||
private emailService: EmailService
|
||||
) {}
|
||||
|
||||
// ==================== Draft Management ====================
|
||||
|
||||
async findDraftsByUserId(userId: string, filters: DraftFilters = {}): Promise<Draft[]> {
|
||||
const { accountId, limit = 50, offset = 0 } = filters;
|
||||
|
||||
let conditions = [eq(drafts.userId, userId)];
|
||||
|
||||
if (accountId) {
|
||||
conditions.push(eq(drafts.accountId, accountId));
|
||||
}
|
||||
|
||||
return this.db
|
||||
.select()
|
||||
.from(drafts)
|
||||
.where(and(...conditions))
|
||||
.orderBy(desc(drafts.updatedAt))
|
||||
.limit(limit)
|
||||
.offset(offset);
|
||||
}
|
||||
|
||||
async findDraftById(id: string, userId: string): Promise<Draft | null> {
|
||||
const [draft] = await this.db
|
||||
.select()
|
||||
.from(drafts)
|
||||
.where(and(eq(drafts.id, id), eq(drafts.userId, userId)));
|
||||
return draft || null;
|
||||
}
|
||||
|
||||
async createDraft(data: NewDraft): Promise<Draft> {
|
||||
const [draft] = await this.db.insert(drafts).values(data).returning();
|
||||
return draft;
|
||||
}
|
||||
|
||||
async updateDraft(id: string, userId: string, data: Partial<NewDraft>): Promise<Draft> {
|
||||
const [draft] = await this.db
|
||||
.update(drafts)
|
||||
.set({ ...data, updatedAt: new Date() })
|
||||
.where(and(eq(drafts.id, id), eq(drafts.userId, userId)))
|
||||
.returning();
|
||||
|
||||
if (!draft) {
|
||||
throw new NotFoundException('Draft not found');
|
||||
}
|
||||
|
||||
return draft;
|
||||
}
|
||||
|
||||
async deleteDraft(id: string, userId: string): Promise<void> {
|
||||
const draft = await this.findDraftById(id, userId);
|
||||
if (!draft) {
|
||||
throw new NotFoundException('Draft not found');
|
||||
}
|
||||
|
||||
await this.db.delete(drafts).where(and(eq(drafts.id, id), eq(drafts.userId, userId)));
|
||||
}
|
||||
|
||||
async countDrafts(userId: string, accountId?: string): Promise<number> {
|
||||
let conditions = [eq(drafts.userId, userId)];
|
||||
|
||||
if (accountId) {
|
||||
conditions.push(eq(drafts.accountId, accountId));
|
||||
}
|
||||
|
||||
const result = await this.db
|
||||
.select({ count: sql<number>`count(*)` })
|
||||
.from(drafts)
|
||||
.where(and(...conditions));
|
||||
|
||||
return Number(result[0]?.count || 0);
|
||||
}
|
||||
|
||||
// ==================== Send Email ====================
|
||||
|
||||
async sendEmail(
|
||||
userId: string,
|
||||
data: {
|
||||
accountId: string;
|
||||
subject?: string;
|
||||
toAddresses: EmailAddress[];
|
||||
ccAddresses?: EmailAddress[];
|
||||
bccAddresses?: EmailAddress[];
|
||||
bodyHtml?: string;
|
||||
bodyPlain?: string;
|
||||
replyToEmailId?: string;
|
||||
replyType?: string;
|
||||
}
|
||||
): Promise<{ success: boolean; messageId?: string }> {
|
||||
// Get the account
|
||||
const account = await this.accountService.findById(data.accountId, userId);
|
||||
if (!account) {
|
||||
throw new NotFoundException('Email account not found');
|
||||
}
|
||||
|
||||
// Build the email
|
||||
const mailOptions: nodemailer.SendMailOptions = {
|
||||
from: {
|
||||
name: account.name,
|
||||
address: account.email,
|
||||
},
|
||||
to: data.toAddresses.map((a) => (a.name ? `"${a.name}" <${a.email}>` : a.email)),
|
||||
cc: data.ccAddresses?.map((a) => (a.name ? `"${a.name}" <${a.email}>` : a.email)),
|
||||
bcc: data.bccAddresses?.map((a) => (a.name ? `"${a.name}" <${a.email}>` : a.email)),
|
||||
subject: data.subject || '(No Subject)',
|
||||
html: data.bodyHtml,
|
||||
text: data.bodyPlain,
|
||||
};
|
||||
|
||||
// Add reply headers if replying
|
||||
if (data.replyToEmailId) {
|
||||
const originalEmail = await this.emailService.findById(data.replyToEmailId, userId);
|
||||
if (originalEmail) {
|
||||
mailOptions.inReplyTo = originalEmail.messageId;
|
||||
mailOptions.references = originalEmail.messageId;
|
||||
}
|
||||
}
|
||||
|
||||
// Send based on provider
|
||||
switch (account.provider) {
|
||||
case 'imap':
|
||||
return this.sendViaSMTP(account, mailOptions);
|
||||
case 'gmail':
|
||||
return this.sendViaGmail(account, mailOptions);
|
||||
case 'outlook':
|
||||
return this.sendViaOutlook(account, mailOptions);
|
||||
default:
|
||||
throw new BadRequestException(`Unknown provider: ${account.provider}`);
|
||||
}
|
||||
}
|
||||
|
||||
async sendDraft(
|
||||
draftId: string,
|
||||
userId: string
|
||||
): Promise<{ success: boolean; messageId?: string }> {
|
||||
const draft = await this.findDraftById(draftId, userId);
|
||||
if (!draft) {
|
||||
throw new NotFoundException('Draft not found');
|
||||
}
|
||||
|
||||
if (!draft.toAddresses || draft.toAddresses.length === 0) {
|
||||
throw new BadRequestException('Draft must have at least one recipient');
|
||||
}
|
||||
|
||||
const result = await this.sendEmail(userId, {
|
||||
accountId: draft.accountId,
|
||||
subject: draft.subject || undefined,
|
||||
toAddresses: draft.toAddresses,
|
||||
ccAddresses: draft.ccAddresses || undefined,
|
||||
bccAddresses: draft.bccAddresses || undefined,
|
||||
bodyHtml: draft.bodyHtml || undefined,
|
||||
bodyPlain: draft.bodyPlain || undefined,
|
||||
replyToEmailId: draft.replyToEmailId || undefined,
|
||||
replyType: draft.replyType || undefined,
|
||||
});
|
||||
|
||||
// Delete draft after successful send
|
||||
if (result.success) {
|
||||
await this.deleteDraft(draftId, userId);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ==================== Provider-specific send methods ====================
|
||||
|
||||
private async sendViaSMTP(
|
||||
account: typeof emailAccounts.$inferSelect,
|
||||
mailOptions: nodemailer.SendMailOptions
|
||||
): Promise<{ success: boolean; messageId?: string }> {
|
||||
if (!account.smtpHost || !account.smtpPort) {
|
||||
throw new BadRequestException('SMTP settings not configured for this account');
|
||||
}
|
||||
|
||||
// Get decrypted password
|
||||
const password = await this.accountService.getDecryptedPassword(account.id, account.userId);
|
||||
if (!password) {
|
||||
throw new BadRequestException('Account password not found');
|
||||
}
|
||||
|
||||
// Create transporter
|
||||
const transporter = nodemailer.createTransport({
|
||||
host: account.smtpHost,
|
||||
port: account.smtpPort,
|
||||
secure: account.smtpSecurity === 'ssl',
|
||||
auth: {
|
||||
user: account.email,
|
||||
pass: password,
|
||||
},
|
||||
tls: {
|
||||
rejectUnauthorized: false, // Allow self-signed certs in dev
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const info = await transporter.sendMail(mailOptions);
|
||||
return { success: true, messageId: info.messageId };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to send email';
|
||||
throw new BadRequestException(`SMTP send failed: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async sendViaGmail(
|
||||
account: typeof emailAccounts.$inferSelect,
|
||||
mailOptions: nodemailer.SendMailOptions
|
||||
): Promise<{ success: boolean; messageId?: string }> {
|
||||
if (!account.accessToken) {
|
||||
throw new BadRequestException('Gmail access token not found');
|
||||
}
|
||||
|
||||
// Use OAuth2 with Gmail
|
||||
const transporter = nodemailer.createTransport({
|
||||
service: 'gmail',
|
||||
auth: {
|
||||
type: 'OAuth2',
|
||||
user: account.email,
|
||||
accessToken: account.accessToken,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
const info = await transporter.sendMail(mailOptions);
|
||||
return { success: true, messageId: info.messageId };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to send email';
|
||||
throw new BadRequestException(`Gmail send failed: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
private async sendViaOutlook(
|
||||
account: typeof emailAccounts.$inferSelect,
|
||||
mailOptions: nodemailer.SendMailOptions
|
||||
): Promise<{ success: boolean; messageId?: string }> {
|
||||
if (!account.accessToken) {
|
||||
throw new BadRequestException('Outlook access token not found');
|
||||
}
|
||||
|
||||
// Use Microsoft Graph API to send
|
||||
const { Client } = await import('@microsoft/microsoft-graph-client');
|
||||
|
||||
const client = Client.init({
|
||||
authProvider: (done) => {
|
||||
done(null, account.accessToken!);
|
||||
},
|
||||
});
|
||||
|
||||
// Convert to Graph API format
|
||||
const message = {
|
||||
subject: mailOptions.subject,
|
||||
body: {
|
||||
contentType: mailOptions.html ? 'HTML' : 'Text',
|
||||
content: mailOptions.html || mailOptions.text || '',
|
||||
},
|
||||
toRecipients: (mailOptions.to as string[])?.map((email) => ({
|
||||
emailAddress: { address: email.replace(/.*<(.+)>/, '$1') },
|
||||
})),
|
||||
ccRecipients: (mailOptions.cc as string[])?.map((email) => ({
|
||||
emailAddress: { address: email.replace(/.*<(.+)>/, '$1') },
|
||||
})),
|
||||
bccRecipients: (mailOptions.bcc as string[])?.map((email) => ({
|
||||
emailAddress: { address: email.replace(/.*<(.+)>/, '$1') },
|
||||
})),
|
||||
};
|
||||
|
||||
try {
|
||||
await client.api('/me/sendMail').post({ message, saveToSentItems: true });
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : 'Failed to send email';
|
||||
throw new BadRequestException(`Outlook send failed: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Reply/Forward Helpers ====================
|
||||
|
||||
async createReplyDraft(
|
||||
userId: string,
|
||||
emailId: string,
|
||||
replyType: 'reply' | 'reply-all' | 'forward'
|
||||
): Promise<Draft> {
|
||||
const originalEmail = await this.emailService.findById(emailId, userId);
|
||||
if (!originalEmail) {
|
||||
throw new NotFoundException('Original email not found');
|
||||
}
|
||||
|
||||
let toAddresses: EmailAddress[] = [];
|
||||
let ccAddresses: EmailAddress[] = [];
|
||||
let subject = originalEmail.subject || '';
|
||||
let bodyHtml = '';
|
||||
|
||||
switch (replyType) {
|
||||
case 'reply':
|
||||
toAddresses = [
|
||||
{ email: originalEmail.fromAddress || '', name: originalEmail.fromName || undefined },
|
||||
];
|
||||
subject = subject.startsWith('Re:') ? subject : `Re: ${subject}`;
|
||||
break;
|
||||
|
||||
case 'reply-all':
|
||||
toAddresses = [
|
||||
{ email: originalEmail.fromAddress || '', name: originalEmail.fromName || undefined },
|
||||
];
|
||||
ccAddresses =
|
||||
originalEmail.toAddresses?.filter((a) => a.email !== originalEmail.fromAddress) || [];
|
||||
if (originalEmail.ccAddresses) {
|
||||
ccAddresses = [...ccAddresses, ...originalEmail.ccAddresses];
|
||||
}
|
||||
subject = subject.startsWith('Re:') ? subject : `Re: ${subject}`;
|
||||
break;
|
||||
|
||||
case 'forward':
|
||||
subject = subject.startsWith('Fwd:') ? subject : `Fwd: ${subject}`;
|
||||
break;
|
||||
}
|
||||
|
||||
// Build quoted content
|
||||
const date = originalEmail.sentAt?.toLocaleString() || 'Unknown date';
|
||||
const from = originalEmail.fromName
|
||||
? `${originalEmail.fromName} <${originalEmail.fromAddress}>`
|
||||
: originalEmail.fromAddress;
|
||||
|
||||
bodyHtml = `
|
||||
<br><br>
|
||||
<div style="border-left: 2px solid #ccc; padding-left: 10px; margin-left: 10px;">
|
||||
<p><strong>On ${date}, ${from} wrote:</strong></p>
|
||||
${originalEmail.bodyHtml || `<pre>${originalEmail.bodyPlain || ''}</pre>`}
|
||||
</div>
|
||||
`;
|
||||
|
||||
return this.createDraft({
|
||||
userId,
|
||||
accountId: originalEmail.accountId,
|
||||
replyToEmailId: emailId,
|
||||
replyType,
|
||||
subject,
|
||||
toAddresses,
|
||||
ccAddresses,
|
||||
bodyHtml,
|
||||
});
|
||||
}
|
||||
}
|
||||
161
apps-archived/mail/apps/backend/src/compose/dto/compose.dto.ts
Normal file
161
apps-archived/mail/apps/backend/src/compose/dto/compose.dto.ts
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
import {
|
||||
IsString,
|
||||
IsOptional,
|
||||
IsUUID,
|
||||
IsArray,
|
||||
IsDateString,
|
||||
ValidateNested,
|
||||
IsEmail,
|
||||
IsIn,
|
||||
} from 'class-validator';
|
||||
import { Type, Transform } from 'class-transformer';
|
||||
|
||||
export class EmailAddressDto {
|
||||
@IsEmail()
|
||||
email: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export class CreateDraftDto {
|
||||
@IsUUID()
|
||||
accountId: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
subject?: string;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EmailAddressDto)
|
||||
@IsOptional()
|
||||
toAddresses?: EmailAddressDto[];
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EmailAddressDto)
|
||||
@IsOptional()
|
||||
ccAddresses?: EmailAddressDto[];
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EmailAddressDto)
|
||||
@IsOptional()
|
||||
bccAddresses?: EmailAddressDto[];
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
bodyHtml?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
bodyPlain?: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
replyToEmailId?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@IsIn(['reply', 'reply-all', 'forward'])
|
||||
replyType?: string;
|
||||
|
||||
@IsDateString()
|
||||
@IsOptional()
|
||||
scheduledAt?: string;
|
||||
}
|
||||
|
||||
export class UpdateDraftDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
subject?: string;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EmailAddressDto)
|
||||
@IsOptional()
|
||||
toAddresses?: EmailAddressDto[];
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EmailAddressDto)
|
||||
@IsOptional()
|
||||
ccAddresses?: EmailAddressDto[];
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EmailAddressDto)
|
||||
@IsOptional()
|
||||
bccAddresses?: EmailAddressDto[];
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
bodyHtml?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
bodyPlain?: string;
|
||||
|
||||
@IsDateString()
|
||||
@IsOptional()
|
||||
scheduledAt?: string;
|
||||
}
|
||||
|
||||
export class SendEmailDto {
|
||||
@IsUUID()
|
||||
accountId: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
subject?: string;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EmailAddressDto)
|
||||
toAddresses: EmailAddressDto[];
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EmailAddressDto)
|
||||
@IsOptional()
|
||||
ccAddresses?: EmailAddressDto[];
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EmailAddressDto)
|
||||
@IsOptional()
|
||||
bccAddresses?: EmailAddressDto[];
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
bodyHtml?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
bodyPlain?: string;
|
||||
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
replyToEmailId?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@IsIn(['reply', 'reply-all', 'forward'])
|
||||
replyType?: string;
|
||||
}
|
||||
|
||||
export class DraftQueryDto {
|
||||
@IsUUID()
|
||||
@IsOptional()
|
||||
accountId?: string;
|
||||
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => parseInt(value, 10))
|
||||
limit?: number;
|
||||
|
||||
@IsOptional()
|
||||
@Transform(({ value }) => parseInt(value, 10))
|
||||
offset?: number;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue