diff --git a/src/controllers/ai-kurzy.controller.js b/src/controllers/ai-kurzy.controller.js
index 4c6b590..71a3727 100644
--- a/src/controllers/ai-kurzy.controller.js
+++ b/src/controllers/ai-kurzy.controller.js
@@ -282,3 +282,22 @@ export const generateCertificate = async (req, res, next) => {
next(error);
}
};
+
+// ==================== CERTIFICATE EMAIL ====================
+
+export const sendCertificateEmail = async (req, res, next) => {
+ try {
+ const { prilohaId } = req.params;
+ const userId = req.user.id;
+ const { sendCertificateEmail: sendCertEmail } = await import('../services/ai-kurzy/certificate-email.service.js');
+
+ const result = await sendCertEmail(parseInt(prilohaId), userId);
+
+ res.json({
+ data: result,
+ message: result.message,
+ });
+ } catch (error) {
+ next(error);
+ }
+};
diff --git a/src/routes/ai-kurzy.routes.js b/src/routes/ai-kurzy.routes.js
index 60c2f7a..e49b23b 100644
--- a/src/routes/ai-kurzy.routes.js
+++ b/src/routes/ai-kurzy.routes.js
@@ -174,4 +174,10 @@ router.post(
aiKurzyController.generateCertificate
);
+router.post(
+ '/prilohy/:prilohaId/send-email',
+ validateParams(prilohaIdSchema),
+ aiKurzyController.sendCertificateEmail
+);
+
export default router;
diff --git a/src/services/ai-kurzy/certificate-email.service.js b/src/services/ai-kurzy/certificate-email.service.js
new file mode 100644
index 0000000..76e76ba
--- /dev/null
+++ b/src/services/ai-kurzy/certificate-email.service.js
@@ -0,0 +1,528 @@
+import fs from 'fs/promises';
+import path from 'path';
+import { db } from '../../config/database.js';
+import { prilohy, registracie, ucastnici, kurzy, emailAccounts } 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
+ const [emailAccount] = await db
+ .select()
+ .from(emailAccounts)
+ .where(and(eq(emailAccounts.userId, userId), eq(emailAccounts.isActive, true)))
+ .limit(1);
+
+ if (!emailAccount) {
+ throw new BadRequestError('Nemáte nastavený aktívny emailový účet pre odosielanie');
+ }
+
+ // 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,
+ };
+};