import fs from 'fs/promises'; import path from 'path'; import { db } from '../../config/database.js'; import { prilohy, registracie, ucastnici, kurzy, emailAccounts, userEmailAccounts } from '../../db/schema.js'; import { eq, and } from 'drizzle-orm'; import { NotFoundError, BadRequestError } from '../../utils/errors.js'; import { logger } from '../../utils/logger.js'; import { jmapRequest, getMailboxes, getIdentities } from '../jmap/client.js'; import { getJmapConfig } from '../jmap/config.js'; /** * Format date to Slovak locale */ const formatDate = (date) => { if (!date) return ''; return new Intl.DateTimeFormat('sk-SK', { day: 'numeric', month: 'long', year: 'numeric', }).format(new Date(date)); }; /** * Generate HTML email template for certificate notification */ export const generateCertificateEmailHtml = ({ participantName, courseName, courseDate, courseType }) => { return ` Certifikát o absolvovaní kurzu
🎓

Gratulujeme k úspešnému absolvovaniu!

Vážený/á ${participantName},

Srdečne Vám ďakujeme za účasť na našom kurze. Úspešne ste ho absolvovali a s potešením Vám zasielame Váš certifikát.

${courseType || 'Kurz'}

${courseName}

📅 ${courseDate}

📎 Príloha: Váš certifikát nájdete v prílohe tohto emailu vo formáte PDF.

Prajeme Vám veľa úspechov pri uplatňovaní nových vedomostí v praxi!

S pozdravom,
Tím AI Kurzov

Táto správa bola automaticky vygenerovaná.
V prípade otázok nás neváhajte kontaktovať.

`.trim(); }; /** * Generate plain text version of the certificate email */ export const generateCertificateEmailText = ({ participantName, courseName, courseDate, courseType }) => { return ` Vážený/á ${participantName}, Srdečne Vám ďakujeme za účasť na našom kurze. Úspešne ste ho absolvovali a s potešením Vám zasielame Váš certifikát. KURZ: ${courseName} TYP: ${courseType || 'Kurz'} DÁTUM: ${courseDate} Váš certifikát nájdete v prílohe tohto emailu vo formáte PDF. Prajeme Vám veľa úspechov pri uplatňovaní nových vedomostí v praxi! S pozdravom, Tím AI Kurzov --- Táto správa bola automaticky vygenerovaná. V prípade otázok nás neváhajte kontaktovať. `.trim(); }; /** * Send email with HTML content and PDF attachment via JMAP */ const sendEmailWithAttachment = async (jmapConfig, to, subject, htmlBody, textBody, attachment) => { logger.info(`Odosielam certifikát emailom na: ${to}`); // Get mailboxes const mailboxes = await getMailboxes(jmapConfig); const sentMailbox = mailboxes.find((m) => m.role === 'sent' || m.name === 'Sent'); if (!sentMailbox) { throw new Error('Priečinok Odoslané nebol nájdený'); } // Read attachment file and encode to base64 const fileBuffer = await fs.readFile(attachment.path); const base64Content = fileBuffer.toString('base64'); // Create email with attachment const createResponse = await jmapRequest(jmapConfig, [ [ 'Email/set', { accountId: jmapConfig.accountId, create: { draft: { mailboxIds: { [sentMailbox.id]: true, }, keywords: { $draft: true, }, from: [{ email: jmapConfig.username }], to: [{ email: to }], subject: subject, htmlBody: [{ partId: 'html', type: 'text/html' }], textBody: [{ partId: 'text', type: 'text/plain' }], attachments: [ { blobId: null, // Will be set via bodyValues type: attachment.mimeType || 'application/pdf', name: attachment.filename, disposition: 'attachment', }, ], bodyValues: { html: { value: htmlBody, }, text: { value: textBody, }, }, }, }, }, 'set1', ], ]); // Check if we need to upload blob first (for some JMAP servers) const createResult = createResponse.methodResponses[0][1]; if (createResult.notCreated?.draft) { // Try alternative approach - upload blob first, then create email logger.info('Skúšam alternatívny prístup s nahraním prílohy'); // Upload blob const uploadResponse = await jmapRequest(jmapConfig, [ [ 'Blob/upload', { accountId: jmapConfig.accountId, create: { attachment1: { data: [{ 'data:asBase64': base64Content }], type: attachment.mimeType || 'application/pdf', }, }, }, 'blob1', ], ]); const blobId = uploadResponse.methodResponses[0][1]?.created?.attachment1?.blobId; if (!blobId) { // Try yet another approach - inline base64 logger.info('Skúšam inline base64 prílohu'); const inlineResponse = await jmapRequest(jmapConfig, [ [ 'Email/set', { accountId: jmapConfig.accountId, create: { draft: { mailboxIds: { [sentMailbox.id]: true, }, keywords: { $draft: true, }, from: [{ email: jmapConfig.username }], to: [{ email: to }], subject: subject, bodyStructure: { type: 'multipart/mixed', subParts: [ { type: 'multipart/alternative', subParts: [ { type: 'text/plain', partId: 'text', }, { type: 'text/html', partId: 'html', }, ], }, { type: attachment.mimeType || 'application/pdf', name: attachment.filename, disposition: 'attachment', partId: 'attachment', }, ], }, bodyValues: { html: { value: htmlBody }, text: { value: textBody }, attachment: { value: base64Content, isEncodingProblem: false }, }, }, }, }, 'set2', ], ]); const inlineResult = inlineResponse.methodResponses[0][1]; const createdEmailId = inlineResult.created?.draft?.id; if (!createdEmailId) { logger.error('Nepodarilo sa vytvoriť email s prílohou', inlineResult.notCreated?.draft); throw new Error('Nepodarilo sa vytvoriť email s prílohou'); } // Submit the email return await submitEmail(jmapConfig, createdEmailId); } // Create email with uploaded blob const emailWithBlobResponse = await jmapRequest(jmapConfig, [ [ 'Email/set', { accountId: jmapConfig.accountId, create: { draft: { mailboxIds: { [sentMailbox.id]: true, }, from: [{ email: jmapConfig.username }], to: [{ email: to }], subject: subject, htmlBody: [{ partId: 'html', type: 'text/html' }], textBody: [{ partId: 'text', type: 'text/plain' }], attachments: [ { blobId: blobId, type: attachment.mimeType || 'application/pdf', name: attachment.filename, disposition: 'attachment', }, ], bodyValues: { html: { value: htmlBody }, text: { value: textBody }, }, }, }, }, 'set3', ], ]); const createdEmailId = emailWithBlobResponse.methodResponses[0][1].created?.draft?.id; if (!createdEmailId) { throw new Error('Nepodarilo sa vytvoriť email s prílohou'); } return await submitEmail(jmapConfig, createdEmailId); } const createdEmailId = createResult.created?.draft?.id; if (!createdEmailId) { throw new Error('Nepodarilo sa vytvoriť email'); } return await submitEmail(jmapConfig, createdEmailId); }; /** * Submit email for sending */ const submitEmail = async (jmapConfig, emailId) => { // Get user identity const identities = await getIdentities(jmapConfig); const identity = identities.find((i) => i.email === jmapConfig.username) || identities[0]; if (!identity) { throw new Error('Nenašla sa identita pre odosielanie emailov'); } // Submit the email const submitResponse = await jmapRequest(jmapConfig, [ [ 'EmailSubmission/set', { accountId: jmapConfig.accountId, create: { submission: { emailId: emailId, identityId: identity.id, }, }, }, 'submit1', ], ]); const submissionId = submitResponse.methodResponses[0][1].created?.submission?.id; if (!submissionId) { const error = submitResponse.methodResponses[0][1].notCreated?.submission; logger.error('Nepodarilo sa odoslať email', error); throw new Error('Nepodarilo sa odoslať email'); } logger.success(`Email s certifikátom úspešne odoslaný`); return { success: true, submissionId }; }; /** * Send certificate email to participant * @param {number} prilohaId - ID of the certificate attachment * @param {number} userId - ID of the user sending the email (for getting email account) */ export const sendCertificateEmail = async (prilohaId, userId) => { // Get priloha with registration and participant info const [priloha] = await db .select({ id: prilohy.id, nazovSuboru: prilohy.nazovSuboru, typPrilohy: prilohy.typPrilohy, cestaKSuboru: prilohy.cestaKSuboru, mimeType: prilohy.mimeType, registraciaId: prilohy.registraciaId, }) .from(prilohy) .where(eq(prilohy.id, prilohaId)) .limit(1); if (!priloha) { throw new NotFoundError('Príloha nenájdená'); } if (priloha.typPrilohy !== 'certifikat') { throw new BadRequestError('Táto príloha nie je certifikát'); } // Check if file exists try { await fs.access(priloha.cestaKSuboru); } catch { throw new NotFoundError('Súbor certifikátu nebol nájdený na serveri'); } // Get registration with participant and course info const [registration] = await db .select({ id: registracie.id, ucastnikMeno: ucastnici.meno, ucastnikPriezvisko: ucastnici.priezvisko, ucastnikTitul: ucastnici.titul, ucastnikEmail: ucastnici.email, kurzNazov: kurzy.nazov, kurzTyp: kurzy.typKurzu, kurzDatumOd: kurzy.datumOd, kurzDatumDo: kurzy.datumDo, }) .from(registracie) .leftJoin(ucastnici, eq(registracie.ucastnikId, ucastnici.id)) .leftJoin(kurzy, eq(registracie.kurzId, kurzy.id)) .where(eq(registracie.id, priloha.registraciaId)) .limit(1); if (!registration) { throw new NotFoundError('Registrácia nenájdená'); } if (!registration.ucastnikEmail) { throw new BadRequestError('Účastník nemá zadaný email'); } // Get email account for sending (through userEmailAccounts join table) const [emailAccountResult] = await db .select({ id: emailAccounts.id, email: emailAccounts.email, emailPassword: emailAccounts.emailPassword, jmapAccountId: emailAccounts.jmapAccountId, isActive: emailAccounts.isActive, }) .from(userEmailAccounts) .innerJoin(emailAccounts, eq(userEmailAccounts.emailAccountId, emailAccounts.id)) .where(and(eq(userEmailAccounts.userId, userId), eq(emailAccounts.isActive, true))) .limit(1); if (!emailAccountResult) { throw new BadRequestError('Nemáte nastavený aktívny emailový účet pre odosielanie'); } const emailAccount = emailAccountResult; // Build participant name const participantName = [ registration.ucastnikTitul, registration.ucastnikMeno, registration.ucastnikPriezvisko, ] .filter(Boolean) .join(' '); // Build course date string const courseDate = registration.kurzDatumOd ? registration.kurzDatumDo && registration.kurzDatumDo !== registration.kurzDatumOd ? `${formatDate(registration.kurzDatumOd)} - ${formatDate(registration.kurzDatumDo)}` : formatDate(registration.kurzDatumOd) : ''; // Generate email content const emailData = { participantName, courseName: registration.kurzNazov || 'Kurz', courseDate, courseType: registration.kurzTyp, }; const htmlBody = generateCertificateEmailHtml(emailData); const textBody = generateCertificateEmailText(emailData); const subject = `Certifikát o absolvovaní kurzu: ${registration.kurzNazov}`; // Get JMAP config const jmapConfig = getJmapConfig(emailAccount, true); // Send email with attachment await sendEmailWithAttachment( jmapConfig, registration.ucastnikEmail, subject, htmlBody, textBody, { path: priloha.cestaKSuboru, filename: priloha.nazovSuboru, mimeType: priloha.mimeType || 'application/pdf', } ); logger.success(`Certifikát odoslaný na ${registration.ucastnikEmail}`); return { success: true, message: `Certifikát bol odoslaný na ${registration.ucastnikEmail}`, recipientEmail: registration.ucastnikEmail, }; };