- validateInput.js: validation error messages - errorHandler.js: unhandled error message - validateBody.js: suspicious input message - crm-email.controller.js: error log messages 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
451 lines
13 KiB
JavaScript
451 lines
13 KiB
JavaScript
import * as crmEmailService from '../services/crm-email.service.js';
|
|
import * as contactService from '../services/contact.service.js';
|
|
import * as emailAccountService from '../services/email-account.service.js';
|
|
import { markEmailAsRead, sendEmail, getJmapConfig, getJmapConfigFromAccount, syncEmailsFromSender, searchEmailsJMAP as searchEmailsJMAPService } from '../services/jmap/index.js';
|
|
import { getUserById } from '../services/auth.service.js';
|
|
import { logger } from '../utils/logger.js';
|
|
|
|
/**
|
|
* Get all emails for authenticated user
|
|
* GET /api/emails?accountId=xxx (REQUIRED)
|
|
*/
|
|
export const getEmails = async (req, res, next) => {
|
|
try {
|
|
const userId = req.userId;
|
|
const { accountId } = req;
|
|
|
|
// Verify user has access to this email account
|
|
await emailAccountService.getEmailAccountById(accountId, userId);
|
|
|
|
const emails = await crmEmailService.getEmailsForAccount(accountId);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
count: emails.length,
|
|
data: emails,
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get emails by thread (conversation)
|
|
* GET /api/emails/thread/:threadId?accountId=xxx (accountId required)
|
|
*/
|
|
export const getThread = async (req, res, next) => {
|
|
try {
|
|
const userId = req.userId;
|
|
const { threadId } = req.params;
|
|
const { accountId } = req;
|
|
|
|
// Verify user has access to this email account
|
|
await emailAccountService.getEmailAccountById(accountId, userId);
|
|
|
|
const thread = await crmEmailService.getEmailThread(accountId, threadId);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
count: thread.length,
|
|
data: thread,
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Search emails
|
|
* GET /api/emails/search?q=query&accountId=xxx (accountId required)
|
|
*/
|
|
export const searchEmails = async (req, res, next) => {
|
|
try {
|
|
const userId = req.userId;
|
|
const { q } = req.query;
|
|
const { accountId } = req;
|
|
|
|
// Verify user has access to this email account
|
|
await emailAccountService.getEmailAccountById(accountId, userId);
|
|
|
|
const results = await crmEmailService.searchEmails(accountId, q);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
count: results.length,
|
|
data: results,
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get unread count
|
|
* GET /api/emails/unread-count
|
|
* Returns total unread count and per-account counts
|
|
*/
|
|
export const getUnreadCount = async (req, res, next) => {
|
|
try {
|
|
const userId = req.userId;
|
|
|
|
// Get all user's email accounts
|
|
const userAccounts = await emailAccountService.getUserEmailAccounts(userId);
|
|
const emailAccountIds = userAccounts.map((account) => account.id);
|
|
|
|
// Get unread count summary for all accounts
|
|
const unreadData = await crmEmailService.getUnreadCountSummary(emailAccountIds);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
data: {
|
|
count: unreadData.totalUnread,
|
|
totalUnread: unreadData.totalUnread,
|
|
accounts: unreadData.byAccount,
|
|
lastUpdatedAt: new Date().toISOString(),
|
|
},
|
|
});
|
|
} catch (error) {
|
|
logger.error('Chyba v getUnreadCount', { error: error.message });
|
|
next(error);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Sync latest emails for all contacts from JMAP
|
|
* POST /api/emails/sync
|
|
* Body: { accountId } (optional - defaults to primary account)
|
|
*/
|
|
export const syncEmails = async (req, res, next) => {
|
|
try {
|
|
const userId = req.userId;
|
|
const { accountId } = req.body;
|
|
|
|
// Get email account (or primary if not specified)
|
|
let emailAccount;
|
|
if (accountId) {
|
|
emailAccount = await emailAccountService.getEmailAccountWithCredentials(accountId, userId);
|
|
} else {
|
|
const primaryAccount = await emailAccountService.getPrimaryEmailAccount(userId);
|
|
if (!primaryAccount) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: {
|
|
message: 'Najprv musíš pripojiť email účet v Profile',
|
|
statusCode: 400,
|
|
},
|
|
});
|
|
}
|
|
emailAccount = await emailAccountService.getEmailAccountWithCredentials(primaryAccount.id, userId);
|
|
}
|
|
|
|
// Get contacts for this email account
|
|
const contacts = await contactService.getContactsForEmailAccount(emailAccount.id);
|
|
|
|
if (!contacts.length) {
|
|
return res.status(200).json({
|
|
success: true,
|
|
message: 'Žiadne kontakty na synchronizáciu',
|
|
data: { contacts: 0, synced: 0, newEmails: 0 },
|
|
});
|
|
}
|
|
|
|
const jmapConfig = getJmapConfigFromAccount(emailAccount);
|
|
let totalSynced = 0;
|
|
let totalNew = 0;
|
|
|
|
for (const contact of contacts) {
|
|
try {
|
|
const { total, saved } = await syncEmailsFromSender(
|
|
jmapConfig,
|
|
emailAccount.id,
|
|
contact.id,
|
|
contact.email,
|
|
{ limit: 50 }
|
|
);
|
|
totalSynced += total;
|
|
totalNew += saved;
|
|
} catch (syncError) {
|
|
logger.error('Nepodarilo sa synchronizovať emaily pre kontakt', { contactEmail: contact.email, error: syncError.message });
|
|
}
|
|
}
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
message: 'Emaily synchronizované',
|
|
data: {
|
|
contacts: contacts.length,
|
|
synced: totalSynced,
|
|
newEmails: totalNew,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Mark email as read/unread
|
|
* PATCH /api/emails/:jmapId/read?accountId=xxx
|
|
*/
|
|
export const markAsRead = async (req, res, next) => {
|
|
try {
|
|
const userId = req.userId;
|
|
const { jmapId } = req.params;
|
|
const { isRead } = req.body;
|
|
const accountId = req.accountId || req.body.accountId;
|
|
|
|
if (!accountId) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: {
|
|
message: 'accountId je povinný parameter',
|
|
statusCode: 400,
|
|
},
|
|
});
|
|
}
|
|
|
|
// Verify user has access to this email account
|
|
const emailAccount = await emailAccountService.getEmailAccountWithCredentials(accountId, userId);
|
|
const jmapConfig = getJmapConfigFromAccount(emailAccount);
|
|
|
|
await markEmailAsRead(jmapConfig, userId, jmapId, isRead);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
message: `Email označený ako ${isRead ? 'prečítaný' : 'neprečítaný'}`,
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Mark all emails from contact as read
|
|
* POST /api/emails/contact/:contactId/read?accountId=xxx
|
|
*/
|
|
export const markContactEmailsRead = async (req, res, next) => {
|
|
try {
|
|
const userId = req.userId;
|
|
const { contactId } = req.params;
|
|
const { accountId } = req;
|
|
|
|
// Verify user has access to this email account
|
|
const emailAccount = await emailAccountService.getEmailAccountWithCredentials(accountId, userId);
|
|
const jmapConfig = getJmapConfigFromAccount(emailAccount);
|
|
|
|
// Mark emails as read in database
|
|
const count = await crmEmailService.markContactEmailsAsRead(contactId, accountId);
|
|
|
|
// Get the emails that were marked as read to sync with JMAP
|
|
const contactEmails = await crmEmailService.getContactEmailsWithUnread(accountId, contactId);
|
|
|
|
// Also mark emails as read on JMAP server
|
|
if (contactEmails && contactEmails.length > 0) {
|
|
logger.info(`Marking ${contactEmails.length} emails as read on JMAP server`);
|
|
|
|
for (const email of contactEmails) {
|
|
if (!email.jmapId || email.isRead) {
|
|
continue;
|
|
}
|
|
try {
|
|
await markEmailAsRead(jmapConfig, userId, email.jmapId, true);
|
|
} catch (jmapError) {
|
|
logger.error('Nepodarilo sa označiť JMAP email ako prečítaný', { jmapId: email.jmapId, error: jmapError.message });
|
|
}
|
|
}
|
|
}
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
message: `Označených ${count} emailov ako prečítaných`,
|
|
data: { count },
|
|
});
|
|
} catch (error) {
|
|
logger.error('Chyba v markContactEmailsRead', { error: error.message });
|
|
next(error);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Mark entire thread as read
|
|
* POST /api/emails/thread/:threadId/read?accountId=xxx
|
|
*/
|
|
export const markThreadRead = async (req, res, next) => {
|
|
try {
|
|
const userId = req.userId;
|
|
const { threadId } = req.params;
|
|
const { accountId } = req;
|
|
|
|
// Verify user has access to this email account
|
|
const emailAccount = await emailAccountService.getEmailAccountWithCredentials(accountId, userId);
|
|
const jmapConfig = getJmapConfigFromAccount(emailAccount);
|
|
|
|
const threadEmails = await crmEmailService.getEmailThread(accountId, threadId);
|
|
const unreadEmails = threadEmails.filter((email) => !email.isRead);
|
|
|
|
// Mark emails as read on JMAP server
|
|
for (const email of unreadEmails) {
|
|
if (!email.jmapId) {
|
|
continue;
|
|
}
|
|
try {
|
|
await markEmailAsRead(jmapConfig, userId, email.jmapId, true);
|
|
} catch (jmapError) {
|
|
logger.error('Nepodarilo sa označiť JMAP email ako prečítaný', { jmapId: email.jmapId, error: jmapError.message });
|
|
}
|
|
}
|
|
|
|
// Mark thread as read in database
|
|
const count = await crmEmailService.markThreadAsRead(accountId, threadId);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
message: 'Konverzácia označená ako prečítaná',
|
|
count,
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Send email reply
|
|
* POST /api/emails/reply
|
|
* Body: { to, subject, body, inReplyTo, threadId, accountId }
|
|
*/
|
|
export const replyToEmail = async (req, res, next) => {
|
|
try {
|
|
const userId = req.userId;
|
|
const { to, subject, body, inReplyTo = null, threadId = null, accountId } = req.body;
|
|
|
|
if (!to || !subject || !body) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: {
|
|
message: 'Chýbajúce povinné polia: to, subject, body',
|
|
statusCode: 400,
|
|
},
|
|
});
|
|
}
|
|
|
|
// Get email account (or primary if not specified)
|
|
let emailAccount;
|
|
if (accountId) {
|
|
emailAccount = await emailAccountService.getEmailAccountWithCredentials(accountId, userId);
|
|
} else {
|
|
const primaryAccount = await emailAccountService.getPrimaryEmailAccount(userId);
|
|
if (!primaryAccount) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: {
|
|
message: 'Najprv musíš pripojiť email účet v Profile',
|
|
statusCode: 400,
|
|
},
|
|
});
|
|
}
|
|
emailAccount = await emailAccountService.getEmailAccountWithCredentials(primaryAccount.id, userId);
|
|
}
|
|
|
|
const jmapConfig = getJmapConfigFromAccount(emailAccount);
|
|
|
|
const result = await sendEmail(jmapConfig, userId, emailAccount.id, to, subject, body, inReplyTo, threadId);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
message: 'Email odoslaný',
|
|
data: result,
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Get emails for a specific contact
|
|
* GET /api/emails/contact/:contactId?accountId=xxx
|
|
*/
|
|
export const getContactEmails = async (req, res, next) => {
|
|
try {
|
|
const userId = req.userId;
|
|
const { contactId } = req.params;
|
|
const accountId = req.accountId || req.query.accountId;
|
|
|
|
if (!accountId) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: {
|
|
message: 'accountId je povinný parameter',
|
|
statusCode: 400,
|
|
},
|
|
});
|
|
}
|
|
|
|
// Verify user has access to this email account
|
|
await emailAccountService.getEmailAccountById(accountId, userId);
|
|
|
|
const emails = await crmEmailService.getContactEmailsWithUnread(accountId, contactId);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
count: emails.length,
|
|
data: emails,
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Search emails using JMAP full-text search
|
|
* GET /api/emails/search-jmap?query=text&limit=50&offset=0&accountId=xxx
|
|
* Searches in: from, to, subject, and email body
|
|
*/
|
|
export const searchEmailsJMAP = async (req, res, next) => {
|
|
try {
|
|
const userId = req.userId;
|
|
const { query = '', limit = 50, offset = 0, accountId } = req.query;
|
|
|
|
logger.debug('searchEmailsJMAP called', { userId, query, limit, offset, accountId });
|
|
|
|
// Get email account (or primary if not specified)
|
|
let emailAccount;
|
|
if (accountId) {
|
|
logger.debug('Using provided accountId', { accountId });
|
|
emailAccount = await emailAccountService.getEmailAccountWithCredentials(accountId, userId);
|
|
} else {
|
|
logger.debug('No accountId provided, using primary account');
|
|
const primaryAccount = await emailAccountService.getPrimaryEmailAccount(userId);
|
|
if (!primaryAccount) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: {
|
|
message: 'Najprv musíš pripojiť email účet v Profile',
|
|
statusCode: 400,
|
|
},
|
|
});
|
|
}
|
|
emailAccount = await emailAccountService.getEmailAccountWithCredentials(primaryAccount.id, userId);
|
|
logger.debug('Using primary account', { accountId: primaryAccount.id });
|
|
}
|
|
|
|
const jmapConfig = getJmapConfigFromAccount(emailAccount);
|
|
|
|
const results = await searchEmailsJMAPService(
|
|
jmapConfig,
|
|
emailAccount.id,
|
|
query,
|
|
parseInt(limit),
|
|
parseInt(offset)
|
|
);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
count: results.length,
|
|
data: results,
|
|
});
|
|
} catch (error) {
|
|
logger.error('Chyba v searchEmailsJMAP', { error: error.message });
|
|
next(error);
|
|
}
|
|
};
|