option for more emails,fix jmap service,add table email accounts
This commit is contained in:
@@ -16,6 +16,7 @@ import authRoutes from './routes/auth.routes.js';
|
|||||||
import adminRoutes from './routes/admin.routes.js';
|
import adminRoutes from './routes/admin.routes.js';
|
||||||
import contactRoutes from './routes/contact.routes.js';
|
import contactRoutes from './routes/contact.routes.js';
|
||||||
import crmEmailRoutes from './routes/crm-email.routes.js';
|
import crmEmailRoutes from './routes/crm-email.routes.js';
|
||||||
|
import emailAccountRoutes from './routes/email-account.routes.js';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
@@ -70,6 +71,7 @@ app.use('/api/auth', authRoutes);
|
|||||||
app.use('/api/admin', adminRoutes);
|
app.use('/api/admin', adminRoutes);
|
||||||
app.use('/api/contacts', contactRoutes);
|
app.use('/api/contacts', contactRoutes);
|
||||||
app.use('/api/emails', crmEmailRoutes);
|
app.use('/api/emails', crmEmailRoutes);
|
||||||
|
app.use('/api/email-accounts', emailAccountRoutes);
|
||||||
|
|
||||||
// Basic route
|
// Basic route
|
||||||
app.get('/', (req, res) => {
|
app.get('/', (req, res) => {
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
import * as contactService from '../services/contact.service.js';
|
import * as contactService from '../services/contact.service.js';
|
||||||
import { discoverContactsFromJMAP, getJmapConfig } from '../services/jmap.service.js';
|
import { discoverContactsFromJMAP, getJmapConfigFromAccount } from '../services/jmap.service.js';
|
||||||
import { formatErrorResponse } from '../utils/errors.js';
|
import { formatErrorResponse } from '../utils/errors.js';
|
||||||
import { getUserById } from '../services/auth.service.js';
|
import * as emailAccountService from '../services/email-account.service.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all contacts for authenticated user
|
* Get all contacts for authenticated user
|
||||||
* GET /api/contacts
|
* GET /api/contacts?accountId=xxx (optional)
|
||||||
*/
|
*/
|
||||||
export const getContacts = async (req, res) => {
|
export const getContacts = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const userId = req.userId;
|
const userId = req.userId;
|
||||||
const contacts = await contactService.getUserContacts(userId);
|
const { accountId } = req.query;
|
||||||
|
|
||||||
|
const contacts = await contactService.getUserContacts(userId, accountId || null);
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -25,18 +27,26 @@ export const getContacts = async (req, res) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Discover potential contacts from JMAP (email senders)
|
* Discover potential contacts from JMAP (email senders)
|
||||||
* GET /api/contacts/discover?search=query&limit=50
|
* GET /api/contacts/discover?accountId=xxx&search=query&limit=50
|
||||||
*/
|
*/
|
||||||
export const discoverContacts = async (req, res) => {
|
export const discoverContacts = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const userId = req.userId;
|
const userId = req.userId;
|
||||||
const { search = '', limit = 50 } = req.query;
|
const { accountId, search = '', limit = 50 } = req.query;
|
||||||
|
|
||||||
// Get user to access JMAP config
|
console.log('🔍 discoverContacts called:', { userId, accountId, search, limit });
|
||||||
const user = await getUserById(userId);
|
|
||||||
|
|
||||||
// Check if user has JMAP email configured
|
// Get email account (or primary if not specified)
|
||||||
if (!user.email || !user.emailPassword || !user.jmapAccountId) {
|
let emailAccount;
|
||||||
|
if (accountId) {
|
||||||
|
console.log('📧 Getting email account by ID:', accountId);
|
||||||
|
emailAccount = await emailAccountService.getEmailAccountWithCredentials(accountId, userId);
|
||||||
|
console.log('✅ Email account retrieved:', { id: emailAccount.id, email: emailAccount.email });
|
||||||
|
} else {
|
||||||
|
console.log('📧 No accountId provided, getting primary account for user:', userId);
|
||||||
|
const primaryAccount = await emailAccountService.getPrimaryEmailAccount(userId);
|
||||||
|
console.log('🔑 Primary account:', primaryAccount ? { id: primaryAccount.id, email: primaryAccount.email } : 'NOT FOUND');
|
||||||
|
if (!primaryAccount) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: {
|
error: {
|
||||||
@@ -45,8 +55,17 @@ export const discoverContacts = async (req, res) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
emailAccount = await emailAccountService.getEmailAccountWithCredentials(primaryAccount.id, userId);
|
||||||
|
console.log('✅ Email account retrieved from primary:', { id: emailAccount.id, email: emailAccount.email });
|
||||||
|
}
|
||||||
|
|
||||||
const jmapConfig = getJmapConfig(user);
|
const jmapConfig = getJmapConfigFromAccount(emailAccount);
|
||||||
|
console.log('🔧 JMAP Config created:', {
|
||||||
|
server: jmapConfig.server,
|
||||||
|
username: jmapConfig.username,
|
||||||
|
accountId: jmapConfig.accountId,
|
||||||
|
hasPassword: !!jmapConfig.password
|
||||||
|
});
|
||||||
|
|
||||||
const potentialContacts = await discoverContactsFromJMAP(
|
const potentialContacts = await discoverContactsFromJMAP(
|
||||||
jmapConfig,
|
jmapConfig,
|
||||||
@@ -61,6 +80,8 @@ export const discoverContacts = async (req, res) => {
|
|||||||
data: potentialContacts,
|
data: potentialContacts,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('❌ ERROR in discoverContacts:', error);
|
||||||
|
console.error('Error stack:', error.stack);
|
||||||
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
|
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
|
||||||
res.status(error.statusCode || 500).json(errorResponse);
|
res.status(error.statusCode || 500).json(errorResponse);
|
||||||
}
|
}
|
||||||
@@ -69,11 +90,15 @@ export const discoverContacts = async (req, res) => {
|
|||||||
/**
|
/**
|
||||||
* Add a new contact
|
* Add a new contact
|
||||||
* POST /api/contacts
|
* POST /api/contacts
|
||||||
|
* Body: { email, name, notes, accountId }
|
||||||
*/
|
*/
|
||||||
export const addContact = async (req, res) => {
|
export const addContact = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const userId = req.userId;
|
const userId = req.userId;
|
||||||
const { email, name = '', notes = '' } = req.body;
|
console.log('📦 Full req.body:', JSON.stringify(req.body, null, 2));
|
||||||
|
const { email, name = '', notes = '', accountId } = req.body;
|
||||||
|
|
||||||
|
console.log('➕ addContact called:', { userId, email, name, accountId });
|
||||||
|
|
||||||
if (!email) {
|
if (!email) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
@@ -85,11 +110,37 @@ export const addContact = async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user to access JMAP config
|
// Get email account (or primary if not specified)
|
||||||
const user = await getUserById(userId);
|
let emailAccount;
|
||||||
const jmapConfig = getJmapConfig(user);
|
if (accountId) {
|
||||||
|
console.log('📧 Using provided accountId:', accountId);
|
||||||
|
emailAccount = await emailAccountService.getEmailAccountWithCredentials(accountId, userId);
|
||||||
|
} else {
|
||||||
|
console.log('📧 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);
|
||||||
|
console.log('📧 Using primary account:', primaryAccount.id);
|
||||||
|
}
|
||||||
|
|
||||||
const contact = await contactService.addContact(userId, jmapConfig, email, name, notes);
|
const jmapConfig = getJmapConfigFromAccount(emailAccount);
|
||||||
|
|
||||||
|
const contact = await contactService.addContact(
|
||||||
|
userId,
|
||||||
|
emailAccount.id,
|
||||||
|
jmapConfig,
|
||||||
|
email,
|
||||||
|
name,
|
||||||
|
notes
|
||||||
|
);
|
||||||
|
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -1,17 +1,20 @@
|
|||||||
import * as crmEmailService from '../services/crm-email.service.js';
|
import * as crmEmailService from '../services/crm-email.service.js';
|
||||||
import * as contactService from '../services/contact.service.js';
|
import * as contactService from '../services/contact.service.js';
|
||||||
import { markEmailAsRead, sendEmail, getJmapConfig, syncEmailsFromSender, searchEmailsJMAP as searchEmailsJMAPService } from '../services/jmap.service.js';
|
import * as emailAccountService from '../services/email-account.service.js';
|
||||||
|
import { markEmailAsRead, sendEmail, getJmapConfig, getJmapConfigFromAccount, syncEmailsFromSender, searchEmailsJMAP as searchEmailsJMAPService } from '../services/jmap.service.js';
|
||||||
import { formatErrorResponse } from '../utils/errors.js';
|
import { formatErrorResponse } from '../utils/errors.js';
|
||||||
import { getUserById } from '../services/auth.service.js';
|
import { getUserById } from '../services/auth.service.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all emails for authenticated user
|
* Get all emails for authenticated user
|
||||||
* GET /api/emails
|
* GET /api/emails?accountId=xxx (optional)
|
||||||
*/
|
*/
|
||||||
export const getEmails = async (req, res) => {
|
export const getEmails = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const userId = req.userId;
|
const userId = req.userId;
|
||||||
const emails = await crmEmailService.getUserEmails(userId);
|
const { accountId } = req.query;
|
||||||
|
|
||||||
|
const emails = await crmEmailService.getUserEmails(userId, accountId || null);
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -48,14 +51,14 @@ export const getThread = async (req, res) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Search emails
|
* Search emails
|
||||||
* GET /api/emails/search?q=query
|
* GET /api/emails/search?q=query&accountId=xxx (accountId optional)
|
||||||
*/
|
*/
|
||||||
export const searchEmails = async (req, res) => {
|
export const searchEmails = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const userId = req.userId;
|
const userId = req.userId;
|
||||||
const { q } = req.query;
|
const { q, accountId } = req.query;
|
||||||
|
|
||||||
const results = await crmEmailService.searchEmails(userId, q);
|
const results = await crmEmailService.searchEmails(userId, q, accountId || null);
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -71,33 +74,24 @@ export const searchEmails = async (req, res) => {
|
|||||||
/**
|
/**
|
||||||
* Get unread count
|
* Get unread count
|
||||||
* GET /api/emails/unread-count
|
* GET /api/emails/unread-count
|
||||||
|
* Returns total unread count and per-account counts
|
||||||
*/
|
*/
|
||||||
export const getUnreadCount = async (req, res) => {
|
export const getUnreadCount = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const userId = req.userId;
|
const userId = req.userId;
|
||||||
const count = await crmEmailService.getUnreadCount(userId);
|
const unreadData = await crmEmailService.getUnreadCount(userId);
|
||||||
|
|
||||||
const accounts = [];
|
|
||||||
|
|
||||||
if (req.user?.email) {
|
|
||||||
accounts.push({
|
|
||||||
id: req.user.jmapAccountId || req.user.email,
|
|
||||||
email: req.user.email,
|
|
||||||
label: req.user.email,
|
|
||||||
unread: count,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
data: {
|
data: {
|
||||||
count,
|
count: unreadData.totalUnread,
|
||||||
totalUnread: count,
|
totalUnread: unreadData.totalUnread,
|
||||||
accounts,
|
accounts: unreadData.accounts,
|
||||||
lastUpdatedAt: new Date().toISOString(),
|
lastUpdatedAt: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('❌ ERROR in getUnreadCount:', error);
|
||||||
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
|
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
|
||||||
res.status(error.statusCode || 500).json(errorResponse);
|
res.status(error.statusCode || 500).json(errorResponse);
|
||||||
}
|
}
|
||||||
@@ -106,13 +100,20 @@ export const getUnreadCount = async (req, res) => {
|
|||||||
/**
|
/**
|
||||||
* Sync latest emails for all contacts from JMAP
|
* Sync latest emails for all contacts from JMAP
|
||||||
* POST /api/emails/sync
|
* POST /api/emails/sync
|
||||||
|
* Body: { accountId } (optional - defaults to primary account)
|
||||||
*/
|
*/
|
||||||
export const syncEmails = async (req, res) => {
|
export const syncEmails = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const userId = req.userId;
|
const userId = req.userId;
|
||||||
const user = await getUserById(userId);
|
const { accountId } = req.body;
|
||||||
|
|
||||||
if (!user.email || !user.emailPassword || !user.jmapAccountId) {
|
// 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({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: {
|
error: {
|
||||||
@@ -121,8 +122,11 @@ export const syncEmails = async (req, res) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
emailAccount = await emailAccountService.getEmailAccountWithCredentials(primaryAccount.id, userId);
|
||||||
|
}
|
||||||
|
|
||||||
const contacts = await contactService.getUserContacts(userId);
|
// Get contacts for this email account
|
||||||
|
const contacts = await contactService.getUserContacts(userId, emailAccount.id);
|
||||||
|
|
||||||
if (!contacts.length) {
|
if (!contacts.length) {
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
@@ -132,7 +136,7 @@ export const syncEmails = async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const jmapConfig = getJmapConfig(user);
|
const jmapConfig = getJmapConfigFromAccount(emailAccount);
|
||||||
let totalSynced = 0;
|
let totalSynced = 0;
|
||||||
let totalNew = 0;
|
let totalNew = 0;
|
||||||
|
|
||||||
@@ -141,6 +145,7 @@ export const syncEmails = async (req, res) => {
|
|||||||
const { total, saved } = await syncEmailsFromSender(
|
const { total, saved } = await syncEmailsFromSender(
|
||||||
jmapConfig,
|
jmapConfig,
|
||||||
userId,
|
userId,
|
||||||
|
emailAccount.id,
|
||||||
contact.id,
|
contact.id,
|
||||||
contact.email,
|
contact.email,
|
||||||
{ limit: 50 }
|
{ limit: 50 }
|
||||||
@@ -240,11 +245,12 @@ export const markThreadRead = async (req, res) => {
|
|||||||
/**
|
/**
|
||||||
* Send email reply
|
* Send email reply
|
||||||
* POST /api/emails/reply
|
* POST /api/emails/reply
|
||||||
|
* Body: { to, subject, body, inReplyTo, threadId, accountId }
|
||||||
*/
|
*/
|
||||||
export const replyToEmail = async (req, res) => {
|
export const replyToEmail = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const userId = req.userId;
|
const userId = req.userId;
|
||||||
const { to, subject, body, inReplyTo = null, threadId = null } = req.body;
|
const { to, subject, body, inReplyTo = null, threadId = null, accountId } = req.body;
|
||||||
|
|
||||||
if (!to || !subject || !body) {
|
if (!to || !subject || !body) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
@@ -256,11 +262,27 @@ export const replyToEmail = async (req, res) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get user to access JMAP config
|
// Get email account (or primary if not specified)
|
||||||
const user = await getUserById(userId);
|
let emailAccount;
|
||||||
const jmapConfig = getJmapConfig(user);
|
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 result = await sendEmail(jmapConfig, userId, to, subject, body, inReplyTo, threadId);
|
const jmapConfig = getJmapConfigFromAccount(emailAccount);
|
||||||
|
|
||||||
|
const result = await sendEmail(jmapConfig, userId, emailAccount.id, to, subject, body, inReplyTo, threadId);
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -297,19 +319,25 @@ export const getContactEmails = async (req, res) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Search emails using JMAP full-text search
|
* Search emails using JMAP full-text search
|
||||||
* GET /api/emails/search-jmap?query=text&limit=50&offset=0
|
* GET /api/emails/search-jmap?query=text&limit=50&offset=0&accountId=xxx
|
||||||
* Searches in: from, to, subject, and email body
|
* Searches in: from, to, subject, and email body
|
||||||
*/
|
*/
|
||||||
export const searchEmailsJMAP = async (req, res) => {
|
export const searchEmailsJMAP = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const userId = req.userId;
|
const userId = req.userId;
|
||||||
const { query = '', limit = 50, offset = 0 } = req.query;
|
const { query = '', limit = 50, offset = 0, accountId } = req.query;
|
||||||
|
|
||||||
// Get user to access JMAP config
|
console.log('🔍 searchEmailsJMAP called:', { userId, query, limit, offset, accountId });
|
||||||
const user = await getUserById(userId);
|
|
||||||
|
|
||||||
// Check if user has JMAP email configured
|
// Get email account (or primary if not specified)
|
||||||
if (!user.email || !user.emailPassword || !user.jmapAccountId) {
|
let emailAccount;
|
||||||
|
if (accountId) {
|
||||||
|
console.log('📧 Using provided accountId:', accountId);
|
||||||
|
emailAccount = await emailAccountService.getEmailAccountWithCredentials(accountId, userId);
|
||||||
|
} else {
|
||||||
|
console.log('📧 No accountId provided, using primary account');
|
||||||
|
const primaryAccount = await emailAccountService.getPrimaryEmailAccount(userId);
|
||||||
|
if (!primaryAccount) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: {
|
error: {
|
||||||
@@ -318,8 +346,11 @@ export const searchEmailsJMAP = async (req, res) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
emailAccount = await emailAccountService.getEmailAccountWithCredentials(primaryAccount.id, userId);
|
||||||
|
console.log('📧 Using primary account:', primaryAccount.id);
|
||||||
|
}
|
||||||
|
|
||||||
const jmapConfig = getJmapConfig(user);
|
const jmapConfig = getJmapConfigFromAccount(emailAccount);
|
||||||
|
|
||||||
const results = await searchEmailsJMAPService(
|
const results = await searchEmailsJMAPService(
|
||||||
jmapConfig,
|
jmapConfig,
|
||||||
@@ -335,6 +366,7 @@ export const searchEmailsJMAP = async (req, res) => {
|
|||||||
data: results,
|
data: results,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('❌ ERROR in searchEmailsJMAP:', error);
|
||||||
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
|
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
|
||||||
res.status(error.statusCode || 500).json(errorResponse);
|
res.status(error.statusCode || 500).json(errorResponse);
|
||||||
}
|
}
|
||||||
|
|||||||
174
src/controllers/email-account.controller.js
Normal file
174
src/controllers/email-account.controller.js
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
import * as emailAccountService from '../services/email-account.service.js';
|
||||||
|
import {
|
||||||
|
logEmailLink,
|
||||||
|
} from '../services/audit.service.js';
|
||||||
|
import { formatErrorResponse } from '../utils/errors.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all email accounts for logged-in user
|
||||||
|
* GET /api/email-accounts
|
||||||
|
*/
|
||||||
|
export const getEmailAccounts = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.userId;
|
||||||
|
const accounts = await emailAccountService.getUserEmailAccounts(userId);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: accounts,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
|
||||||
|
res.status(error.statusCode || 500).json(errorResponse);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific email account
|
||||||
|
* GET /api/email-accounts/:id
|
||||||
|
*/
|
||||||
|
export const getEmailAccount = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.userId;
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const account = await emailAccountService.getEmailAccountById(id, userId);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: account,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
|
||||||
|
res.status(error.statusCode || 500).json(errorResponse);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new email account
|
||||||
|
* POST /api/email-accounts
|
||||||
|
*/
|
||||||
|
export const createEmailAccount = async (req, res) => {
|
||||||
|
const { email, emailPassword } = req.body;
|
||||||
|
const userId = req.userId;
|
||||||
|
const ipAddress = req.ip || req.connection.remoteAddress;
|
||||||
|
const userAgent = req.headers['user-agent'];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const account = await emailAccountService.createEmailAccount(
|
||||||
|
userId,
|
||||||
|
email,
|
||||||
|
emailPassword
|
||||||
|
);
|
||||||
|
|
||||||
|
// Log email account creation
|
||||||
|
await logEmailLink(userId, email, ipAddress, userAgent);
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
success: true,
|
||||||
|
data: account,
|
||||||
|
message: 'Email účet úspešne pripojený',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
|
||||||
|
res.status(error.statusCode || 500).json(errorResponse);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update email account password
|
||||||
|
* PATCH /api/email-accounts/:id/password
|
||||||
|
*/
|
||||||
|
export const updateEmailAccountPassword = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.userId;
|
||||||
|
const { id } = req.params;
|
||||||
|
const { emailPassword } = req.body;
|
||||||
|
|
||||||
|
const result = await emailAccountService.updateEmailAccountPassword(
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
emailPassword
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: result,
|
||||||
|
message: 'Heslo k emailovému účtu bolo aktualizované',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
|
||||||
|
res.status(error.statusCode || 500).json(errorResponse);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle email account active status
|
||||||
|
* PATCH /api/email-accounts/:id/status
|
||||||
|
*/
|
||||||
|
export const toggleEmailAccountStatus = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.userId;
|
||||||
|
const { id } = req.params;
|
||||||
|
const { isActive } = req.body;
|
||||||
|
|
||||||
|
const result = await emailAccountService.toggleEmailAccountStatus(
|
||||||
|
id,
|
||||||
|
userId,
|
||||||
|
isActive
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: result,
|
||||||
|
message: `Email účet ${isActive ? 'aktivovaný' : 'deaktivovaný'}`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
|
||||||
|
res.status(error.statusCode || 500).json(errorResponse);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set email account as primary
|
||||||
|
* POST /api/email-accounts/:id/set-primary
|
||||||
|
*/
|
||||||
|
export const setPrimaryEmailAccount = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.userId;
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const result = await emailAccountService.setPrimaryEmailAccount(id, userId);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: result,
|
||||||
|
message: 'Primárny email účet bol nastavený',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
|
||||||
|
res.status(error.statusCode || 500).json(errorResponse);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete email account
|
||||||
|
* DELETE /api/email-accounts/:id
|
||||||
|
*/
|
||||||
|
export const deleteEmailAccount = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const userId = req.userId;
|
||||||
|
const { id } = req.params;
|
||||||
|
|
||||||
|
const result = await emailAccountService.deleteEmailAccount(id, userId);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
data: result,
|
||||||
|
message: result.message,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const errorResponse = formatErrorResponse(error, process.env.NODE_ENV === 'development');
|
||||||
|
res.status(error.statusCode || 500).json(errorResponse);
|
||||||
|
}
|
||||||
|
};
|
||||||
17
src/db/migrations/0002_parallel_guardian.sql
Normal file
17
src/db/migrations/0002_parallel_guardian.sql
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
CREATE TABLE "email_accounts" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"user_id" uuid NOT NULL,
|
||||||
|
"email" text NOT NULL,
|
||||||
|
"email_password" text NOT NULL,
|
||||||
|
"jmap_account_id" text NOT NULL,
|
||||||
|
"is_primary" boolean DEFAULT false NOT NULL,
|
||||||
|
"is_active" boolean DEFAULT true NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "contacts" ADD COLUMN "email_account_id" uuid NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "emails" ADD COLUMN "email_account_id" uuid NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "email_accounts" ADD CONSTRAINT "email_accounts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "contacts" ADD CONSTRAINT "contacts_email_account_id_email_accounts_id_fk" FOREIGN KEY ("email_account_id") REFERENCES "public"."email_accounts"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||||
|
ALTER TABLE "emails" ADD CONSTRAINT "emails_email_account_id_email_accounts_id_fk" FOREIGN KEY ("email_account_id") REFERENCES "public"."email_accounts"("id") ON DELETE cascade ON UPDATE no action;
|
||||||
600
src/db/migrations/meta/0002_snapshot.json
Normal file
600
src/db/migrations/meta/0002_snapshot.json
Normal file
@@ -0,0 +1,600 @@
|
|||||||
|
{
|
||||||
|
"id": "0a729a36-e7a3-488d-b9c5-26392e1cc67d",
|
||||||
|
"prevId": "1b8c1e0f-8476-470c-a641-b3c350a2c1a4",
|
||||||
|
"version": "7",
|
||||||
|
"dialect": "postgresql",
|
||||||
|
"tables": {
|
||||||
|
"public.audit_logs": {
|
||||||
|
"name": "audit_logs",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"name": "action",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"resource": {
|
||||||
|
"name": "resource",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"resource_id": {
|
||||||
|
"name": "resource_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"old_value": {
|
||||||
|
"name": "old_value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"new_value": {
|
||||||
|
"name": "new_value",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"ip_address": {
|
||||||
|
"name": "ip_address",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"user_agent": {
|
||||||
|
"name": "user_agent",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"name": "success",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"error_message": {
|
||||||
|
"name": "error_message",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"audit_logs_user_id_users_id_fk": {
|
||||||
|
"name": "audit_logs_user_id_users_id_fk",
|
||||||
|
"tableFrom": "audit_logs",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "set null",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.contacts": {
|
||||||
|
"name": "contacts",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email_account_id": {
|
||||||
|
"name": "email_account_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"name": {
|
||||||
|
"name": "name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"notes": {
|
||||||
|
"name": "notes",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"added_at": {
|
||||||
|
"name": "added_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"contacts_user_id_users_id_fk": {
|
||||||
|
"name": "contacts_user_id_users_id_fk",
|
||||||
|
"tableFrom": "contacts",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"contacts_email_account_id_email_accounts_id_fk": {
|
||||||
|
"name": "contacts_email_account_id_email_accounts_id_fk",
|
||||||
|
"tableFrom": "contacts",
|
||||||
|
"tableTo": "email_accounts",
|
||||||
|
"columnsFrom": [
|
||||||
|
"email_account_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.email_accounts": {
|
||||||
|
"name": "email_accounts",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email_password": {
|
||||||
|
"name": "email_password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"jmap_account_id": {
|
||||||
|
"name": "jmap_account_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"is_primary": {
|
||||||
|
"name": "is_primary",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"is_active": {
|
||||||
|
"name": "is_active",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"email_accounts_user_id_users_id_fk": {
|
||||||
|
"name": "email_accounts_user_id_users_id_fk",
|
||||||
|
"tableFrom": "email_accounts",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.emails": {
|
||||||
|
"name": "emails",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email_account_id": {
|
||||||
|
"name": "email_account_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"contact_id": {
|
||||||
|
"name": "contact_id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"jmap_id": {
|
||||||
|
"name": "jmap_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"message_id": {
|
||||||
|
"name": "message_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"thread_id": {
|
||||||
|
"name": "thread_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"in_reply_to": {
|
||||||
|
"name": "in_reply_to",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"from": {
|
||||||
|
"name": "from",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"to": {
|
||||||
|
"name": "to",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"subject": {
|
||||||
|
"name": "subject",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"body": {
|
||||||
|
"name": "body",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"is_read": {
|
||||||
|
"name": "is_read",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"name": "date",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"emails_user_id_users_id_fk": {
|
||||||
|
"name": "emails_user_id_users_id_fk",
|
||||||
|
"tableFrom": "emails",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"emails_email_account_id_email_accounts_id_fk": {
|
||||||
|
"name": "emails_email_account_id_email_accounts_id_fk",
|
||||||
|
"tableFrom": "emails",
|
||||||
|
"tableTo": "email_accounts",
|
||||||
|
"columnsFrom": [
|
||||||
|
"email_account_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
},
|
||||||
|
"emails_contact_id_contacts_id_fk": {
|
||||||
|
"name": "emails_contact_id_contacts_id_fk",
|
||||||
|
"tableFrom": "emails",
|
||||||
|
"tableTo": "contacts",
|
||||||
|
"columnsFrom": [
|
||||||
|
"contact_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "no action"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"emails_jmap_id_unique": {
|
||||||
|
"name": "emails_jmap_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"jmap_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"emails_message_id_unique": {
|
||||||
|
"name": "emails_message_id_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"message_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
},
|
||||||
|
"public.users": {
|
||||||
|
"name": "users",
|
||||||
|
"schema": "",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "uuid",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "gen_random_uuid()"
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"name": "username",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"email_password": {
|
||||||
|
"name": "email_password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"jmap_account_id": {
|
||||||
|
"name": "jmap_account_id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"first_name": {
|
||||||
|
"name": "first_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"last_name": {
|
||||||
|
"name": "last_name",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"password": {
|
||||||
|
"name": "password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"temp_password": {
|
||||||
|
"name": "temp_password",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"changed_password": {
|
||||||
|
"name": "changed_password",
|
||||||
|
"type": "boolean",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"role": {
|
||||||
|
"name": "role",
|
||||||
|
"type": "role",
|
||||||
|
"typeSchema": "public",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "'member'"
|
||||||
|
},
|
||||||
|
"last_login": {
|
||||||
|
"name": "last_login",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"name": "created_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"name": "updated_at",
|
||||||
|
"type": "timestamp",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"default": "now()"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {
|
||||||
|
"users_username_unique": {
|
||||||
|
"name": "users_username_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"username"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"users_email_unique": {
|
||||||
|
"name": "users_email_unique",
|
||||||
|
"nullsNotDistinct": false,
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policies": {},
|
||||||
|
"checkConstraints": {},
|
||||||
|
"isRLSEnabled": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"enums": {
|
||||||
|
"public.role": {
|
||||||
|
"name": "role",
|
||||||
|
"schema": "public",
|
||||||
|
"values": [
|
||||||
|
"admin",
|
||||||
|
"member"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"schemas": {},
|
||||||
|
"sequences": {},
|
||||||
|
"roles": {},
|
||||||
|
"policies": {},
|
||||||
|
"views": {},
|
||||||
|
"_meta": {
|
||||||
|
"columns": {},
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,13 @@
|
|||||||
"when": 1763457837858,
|
"when": 1763457837858,
|
||||||
"tag": "0001_slow_drax",
|
"tag": "0001_slow_drax",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 2,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1763547133084,
|
||||||
|
"tag": "0002_parallel_guardian",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
138
src/db/migrations/migrate-data-only.js
Normal file
138
src/db/migrations/migrate-data-only.js
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||||
|
import pkg from 'pg';
|
||||||
|
const { Pool } = pkg;
|
||||||
|
import { sql } from 'drizzle-orm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data-only migration script to move existing user emails to email_accounts
|
||||||
|
* Assumes tables already exist from Drizzle migrations
|
||||||
|
*/
|
||||||
|
|
||||||
|
const pool = new Pool({
|
||||||
|
host: process.env.DB_HOST || 'localhost',
|
||||||
|
port: parseInt(process.env.DB_PORT || '5432'),
|
||||||
|
user: process.env.DB_USER || 'admin',
|
||||||
|
password: process.env.DB_PASSWORD || 'heslo123',
|
||||||
|
database: process.env.DB_NAME || 'crm',
|
||||||
|
});
|
||||||
|
|
||||||
|
const db = drizzle(pool);
|
||||||
|
|
||||||
|
async function migrateData() {
|
||||||
|
console.log('🚀 Starting data migration to email accounts...\n');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Step 1: Check if email_accounts table exists
|
||||||
|
console.log('Step 1: Checking email_accounts table...');
|
||||||
|
const tableExists = await db.execute(sql`
|
||||||
|
SELECT EXISTS (
|
||||||
|
SELECT FROM information_schema.tables
|
||||||
|
WHERE table_schema = 'public'
|
||||||
|
AND table_name = 'email_accounts'
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (!tableExists.rows[0].exists) {
|
||||||
|
throw new Error('email_accounts table does not exist. Run Drizzle migrations first.');
|
||||||
|
}
|
||||||
|
console.log('✅ email_accounts table exists\n');
|
||||||
|
|
||||||
|
// Step 2: Migrate existing user emails to email_accounts
|
||||||
|
console.log('Step 2: Migrating existing user emails to email_accounts...');
|
||||||
|
const usersWithEmail = await db.execute(sql`
|
||||||
|
SELECT id, email, email_password, jmap_account_id
|
||||||
|
FROM users
|
||||||
|
WHERE email IS NOT NULL
|
||||||
|
AND email_password IS NOT NULL
|
||||||
|
AND jmap_account_id IS NOT NULL
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log(`Found ${usersWithEmail.rows.length} users with email accounts`);
|
||||||
|
|
||||||
|
for (const user of usersWithEmail.rows) {
|
||||||
|
// Check if already migrated
|
||||||
|
const existing = await db.execute(sql`
|
||||||
|
SELECT id FROM email_accounts
|
||||||
|
WHERE user_id = ${user.id} AND email = ${user.email}
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (existing.rows.length > 0) {
|
||||||
|
console.log(` ⏩ Skipping user ${user.id}: ${user.email} (already migrated)`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
INSERT INTO email_accounts (user_id, email, email_password, jmap_account_id, is_primary, is_active)
|
||||||
|
VALUES (${user.id}, ${user.email}, ${user.email_password}, ${user.jmap_account_id}, true, true)
|
||||||
|
`);
|
||||||
|
console.log(` ✓ Migrated email account for user ${user.id}: ${user.email}`);
|
||||||
|
}
|
||||||
|
console.log('✅ User emails migrated\n');
|
||||||
|
|
||||||
|
// Step 3: Update existing contacts with email_account_id
|
||||||
|
console.log('Step 3: Updating existing contacts with email_account_id...');
|
||||||
|
const contactsNeedUpdate = await db.execute(sql`
|
||||||
|
SELECT COUNT(*) as count FROM contacts WHERE email_account_id IS NULL
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (parseInt(contactsNeedUpdate.rows[0].count) > 0) {
|
||||||
|
await db.execute(sql`
|
||||||
|
UPDATE contacts
|
||||||
|
SET email_account_id = (
|
||||||
|
SELECT ea.id
|
||||||
|
FROM email_accounts ea
|
||||||
|
WHERE ea.user_id = contacts.user_id
|
||||||
|
AND ea.is_primary = true
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
WHERE email_account_id IS NULL
|
||||||
|
`);
|
||||||
|
console.log(`✅ Updated ${contactsNeedUpdate.rows[0].count} contacts\n`);
|
||||||
|
} else {
|
||||||
|
console.log('✅ No contacts to update\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 4: Update existing emails with email_account_id
|
||||||
|
console.log('Step 4: Updating existing emails with email_account_id...');
|
||||||
|
const emailsNeedUpdate = await db.execute(sql`
|
||||||
|
SELECT COUNT(*) as count FROM emails WHERE email_account_id IS NULL
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (parseInt(emailsNeedUpdate.rows[0].count) > 0) {
|
||||||
|
await db.execute(sql`
|
||||||
|
UPDATE emails
|
||||||
|
SET email_account_id = (
|
||||||
|
SELECT ea.id
|
||||||
|
FROM email_accounts ea
|
||||||
|
WHERE ea.user_id = emails.user_id
|
||||||
|
AND ea.is_primary = true
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
WHERE email_account_id IS NULL
|
||||||
|
`);
|
||||||
|
console.log(`✅ Updated ${emailsNeedUpdate.rows[0].count} emails\n`);
|
||||||
|
} else {
|
||||||
|
console.log('✅ No emails to update\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Summary
|
||||||
|
console.log('🎉 Data migration completed successfully!\n');
|
||||||
|
console.log('Summary:');
|
||||||
|
console.log(` - Email accounts migrated: ${usersWithEmail.rows.length}`);
|
||||||
|
console.log(` - Contacts updated: ${contactsNeedUpdate.rows[0].count}`);
|
||||||
|
console.log(` - Emails updated: ${emailsNeedUpdate.rows[0].count}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Migration failed:', error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run migration
|
||||||
|
migrateData().catch((error) => {
|
||||||
|
console.error('Fatal error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
179
src/db/migrations/migrate-to-email-accounts.js
Normal file
179
src/db/migrations/migrate-to-email-accounts.js
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
import 'dotenv/config';
|
||||||
|
import { drizzle } from 'drizzle-orm/node-postgres';
|
||||||
|
import pkg from 'pg';
|
||||||
|
const { Pool } = pkg;
|
||||||
|
import { users, emailAccounts, contacts, emails } from '../schema.js';
|
||||||
|
import { sql } from 'drizzle-orm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Migration script to move from single email per user to multiple email accounts
|
||||||
|
*
|
||||||
|
* Steps:
|
||||||
|
* 1. Create email_accounts table
|
||||||
|
* 2. Migrate existing user emails to email_accounts (as primary)
|
||||||
|
* 3. Add email_account_id to contacts and emails tables
|
||||||
|
* 4. Update existing contacts and emails to reference new email accounts
|
||||||
|
*/
|
||||||
|
|
||||||
|
const pool = new Pool({
|
||||||
|
host: process.env.DB_HOST || 'localhost',
|
||||||
|
port: parseInt(process.env.DB_PORT || '5432'),
|
||||||
|
user: process.env.DB_USER || 'admin',
|
||||||
|
password: process.env.DB_PASSWORD || 'heslo123',
|
||||||
|
database: process.env.DB_NAME || 'crm',
|
||||||
|
});
|
||||||
|
|
||||||
|
const db = drizzle(pool);
|
||||||
|
|
||||||
|
async function migrateToEmailAccounts() {
|
||||||
|
console.log('🚀 Starting migration to email accounts...\n');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Step 1: Create email_accounts table
|
||||||
|
console.log('Step 1: Creating email_accounts table...');
|
||||||
|
await db.execute(sql`
|
||||||
|
CREATE TABLE IF NOT EXISTS "email_accounts" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"user_id" uuid NOT NULL,
|
||||||
|
"email" text NOT NULL,
|
||||||
|
"email_password" text NOT NULL,
|
||||||
|
"jmap_account_id" text NOT NULL,
|
||||||
|
"is_primary" boolean DEFAULT false NOT NULL,
|
||||||
|
"is_active" boolean DEFAULT true NOT NULL,
|
||||||
|
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||||
|
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE "email_accounts"
|
||||||
|
ADD CONSTRAINT "email_accounts_user_id_users_id_fk"
|
||||||
|
FOREIGN KEY ("user_id") REFERENCES "public"."users"("id")
|
||||||
|
ON DELETE cascade ON UPDATE no action
|
||||||
|
`);
|
||||||
|
console.log('✅ email_accounts table created\n');
|
||||||
|
|
||||||
|
// Step 2: Migrate existing user emails to email_accounts
|
||||||
|
console.log('Step 2: Migrating existing user emails to email_accounts...');
|
||||||
|
const usersWithEmail = await db.execute(sql`
|
||||||
|
SELECT id, email, email_password, jmap_account_id
|
||||||
|
FROM users
|
||||||
|
WHERE email IS NOT NULL
|
||||||
|
AND email_password IS NOT NULL
|
||||||
|
AND jmap_account_id IS NOT NULL
|
||||||
|
`);
|
||||||
|
|
||||||
|
console.log(`Found ${usersWithEmail.rows.length} users with email accounts`);
|
||||||
|
|
||||||
|
for (const user of usersWithEmail.rows) {
|
||||||
|
await db.execute(sql`
|
||||||
|
INSERT INTO email_accounts (user_id, email, email_password, jmap_account_id, is_primary, is_active)
|
||||||
|
VALUES (${user.id}, ${user.email}, ${user.email_password}, ${user.jmap_account_id}, true, true)
|
||||||
|
`);
|
||||||
|
console.log(` ✓ Migrated email account for user ${user.id}: ${user.email}`);
|
||||||
|
}
|
||||||
|
console.log('✅ User emails migrated\n');
|
||||||
|
|
||||||
|
// Step 3: Add email_account_id column to contacts (nullable first)
|
||||||
|
console.log('Step 3: Adding email_account_id to contacts table...');
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE contacts
|
||||||
|
ADD COLUMN IF NOT EXISTS email_account_id uuid
|
||||||
|
`);
|
||||||
|
console.log('✅ Column added to contacts\n');
|
||||||
|
|
||||||
|
// Step 4: Update existing contacts with email_account_id
|
||||||
|
console.log('Step 4: Updating existing contacts with email_account_id...');
|
||||||
|
await db.execute(sql`
|
||||||
|
UPDATE contacts
|
||||||
|
SET email_account_id = (
|
||||||
|
SELECT ea.id
|
||||||
|
FROM email_accounts ea
|
||||||
|
WHERE ea.user_id = contacts.user_id
|
||||||
|
AND ea.is_primary = true
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
WHERE email_account_id IS NULL
|
||||||
|
`);
|
||||||
|
|
||||||
|
const contactsUpdated = await db.execute(sql`
|
||||||
|
SELECT COUNT(*) as count FROM contacts WHERE email_account_id IS NOT NULL
|
||||||
|
`);
|
||||||
|
console.log(`✅ Updated ${contactsUpdated.rows[0].count} contacts\n`);
|
||||||
|
|
||||||
|
// Step 5: Make email_account_id NOT NULL and add foreign key
|
||||||
|
console.log('Step 5: Adding constraints to contacts...');
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE contacts
|
||||||
|
ALTER COLUMN email_account_id SET NOT NULL
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE contacts
|
||||||
|
ADD CONSTRAINT "contacts_email_account_id_email_accounts_id_fk"
|
||||||
|
FOREIGN KEY ("email_account_id") REFERENCES "public"."email_accounts"("id")
|
||||||
|
ON DELETE cascade ON UPDATE no action
|
||||||
|
`);
|
||||||
|
console.log('✅ Constraints added to contacts\n');
|
||||||
|
|
||||||
|
// Step 6: Add email_account_id column to emails (nullable first)
|
||||||
|
console.log('Step 6: Adding email_account_id to emails table...');
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE emails
|
||||||
|
ADD COLUMN IF NOT EXISTS email_account_id uuid
|
||||||
|
`);
|
||||||
|
console.log('✅ Column added to emails\n');
|
||||||
|
|
||||||
|
// Step 7: Update existing emails with email_account_id
|
||||||
|
console.log('Step 7: Updating existing emails with email_account_id...');
|
||||||
|
await db.execute(sql`
|
||||||
|
UPDATE emails
|
||||||
|
SET email_account_id = (
|
||||||
|
SELECT ea.id
|
||||||
|
FROM email_accounts ea
|
||||||
|
WHERE ea.user_id = emails.user_id
|
||||||
|
AND ea.is_primary = true
|
||||||
|
LIMIT 1
|
||||||
|
)
|
||||||
|
WHERE email_account_id IS NULL
|
||||||
|
`);
|
||||||
|
|
||||||
|
const emailsUpdated = await db.execute(sql`
|
||||||
|
SELECT COUNT(*) as count FROM emails WHERE email_account_id IS NOT NULL
|
||||||
|
`);
|
||||||
|
console.log(`✅ Updated ${emailsUpdated.rows[0].count} emails\n`);
|
||||||
|
|
||||||
|
// Step 8: Make email_account_id NOT NULL and add foreign key
|
||||||
|
console.log('Step 8: Adding constraints to emails...');
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE emails
|
||||||
|
ALTER COLUMN email_account_id SET NOT NULL
|
||||||
|
`);
|
||||||
|
|
||||||
|
await db.execute(sql`
|
||||||
|
ALTER TABLE emails
|
||||||
|
ADD CONSTRAINT "emails_email_account_id_email_accounts_id_fk"
|
||||||
|
FOREIGN KEY ("email_account_id") REFERENCES "public"."email_accounts"("id")
|
||||||
|
ON DELETE cascade ON UPDATE no action
|
||||||
|
`);
|
||||||
|
console.log('✅ Constraints added to emails\n');
|
||||||
|
|
||||||
|
console.log('🎉 Migration completed successfully!\n');
|
||||||
|
console.log('Summary:');
|
||||||
|
console.log(` - Email accounts created: ${usersWithEmail.rows.length}`);
|
||||||
|
console.log(` - Contacts updated: ${contactsUpdated.rows[0].count}`);
|
||||||
|
console.log(` - Emails updated: ${emailsUpdated.rows[0].count}`);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('❌ Migration failed:', error);
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
await pool.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run migration
|
||||||
|
migrateToEmailAccounts().catch((error) => {
|
||||||
|
console.error('Fatal error:', error);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
@@ -21,6 +21,19 @@ export const users = pgTable('users', {
|
|||||||
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Email Accounts table - viacero emailových účtov pre jedného usera
|
||||||
|
export const emailAccounts = pgTable('email_accounts', {
|
||||||
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
|
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
||||||
|
email: text('email').notNull(),
|
||||||
|
emailPassword: text('email_password').notNull(), // Heslo k emailovému účtu (encrypted)
|
||||||
|
jmapAccountId: text('jmap_account_id').notNull(), // JMAP account ID z truemail
|
||||||
|
isPrimary: boolean('is_primary').default(false).notNull(), // primárny email účet
|
||||||
|
isActive: boolean('is_active').default(true).notNull(), // či je účet aktívny
|
||||||
|
createdAt: timestamp('created_at').defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp('updated_at').defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
// Audit logs - kompletný audit trail všetkých akcií
|
// Audit logs - kompletný audit trail všetkých akcií
|
||||||
export const auditLogs = pgTable('audit_logs', {
|
export const auditLogs = pgTable('audit_logs', {
|
||||||
id: uuid('id').primaryKey().defaultRandom(),
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
@@ -41,6 +54,7 @@ export const auditLogs = pgTable('audit_logs', {
|
|||||||
export const contacts = pgTable('contacts', {
|
export const contacts = pgTable('contacts', {
|
||||||
id: uuid('id').primaryKey().defaultRandom(),
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
||||||
|
emailAccountId: uuid('email_account_id').references(() => emailAccounts.id, { onDelete: 'cascade' }).notNull(),
|
||||||
email: text('email').notNull(),
|
email: text('email').notNull(),
|
||||||
name: text('name'),
|
name: text('name'),
|
||||||
notes: text('notes'),
|
notes: text('notes'),
|
||||||
@@ -53,6 +67,7 @@ export const contacts = pgTable('contacts', {
|
|||||||
export const emails = pgTable('emails', {
|
export const emails = pgTable('emails', {
|
||||||
id: uuid('id').primaryKey().defaultRandom(),
|
id: uuid('id').primaryKey().defaultRandom(),
|
||||||
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
userId: uuid('user_id').references(() => users.id, { onDelete: 'cascade' }).notNull(),
|
||||||
|
emailAccountId: uuid('email_account_id').references(() => emailAccounts.id, { onDelete: 'cascade' }).notNull(),
|
||||||
contactId: uuid('contact_id').references(() => contacts.id, { onDelete: 'cascade' }),
|
contactId: uuid('contact_id').references(() => contacts.id, { onDelete: 'cascade' }),
|
||||||
jmapId: text('jmap_id').unique(),
|
jmapId: text('jmap_id').unique(),
|
||||||
messageId: text('message_id').unique(),
|
messageId: text('message_id').unique(),
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ router.post(
|
|||||||
email: z.string().email('Neplatný formát emailu'),
|
email: z.string().email('Neplatný formát emailu'),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
notes: z.string().optional(),
|
notes: z.string().optional(),
|
||||||
|
accountId: z.string().uuid().optional(),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
contactController.addContact
|
contactController.addContact
|
||||||
|
|||||||
71
src/routes/email-account.routes.js
Normal file
71
src/routes/email-account.routes.js
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import * as emailAccountController from '../controllers/email-account.controller.js';
|
||||||
|
import { authenticate } from '../middlewares/auth/authMiddleware.js';
|
||||||
|
import { validateBody, validateParams } from '../middlewares/security/validateInput.js';
|
||||||
|
import { sensitiveOperationLimiter } from '../middlewares/security/rateLimiter.js';
|
||||||
|
import {
|
||||||
|
createEmailAccountSchema,
|
||||||
|
updateEmailAccountSchema,
|
||||||
|
} from '../validators/email-account.validators.js';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
// All email account routes require authentication
|
||||||
|
router.use(authenticate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Email account management
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Get all email accounts for logged-in user
|
||||||
|
router.get('/', emailAccountController.getEmailAccounts);
|
||||||
|
|
||||||
|
// Get specific email account
|
||||||
|
router.get(
|
||||||
|
'/:id',
|
||||||
|
validateParams(z.object({ id: z.string().uuid() })),
|
||||||
|
emailAccountController.getEmailAccount
|
||||||
|
);
|
||||||
|
|
||||||
|
// Create new email account
|
||||||
|
router.post(
|
||||||
|
'/',
|
||||||
|
sensitiveOperationLimiter,
|
||||||
|
validateBody(createEmailAccountSchema),
|
||||||
|
emailAccountController.createEmailAccount
|
||||||
|
);
|
||||||
|
|
||||||
|
// Update email account password
|
||||||
|
router.patch(
|
||||||
|
'/:id/password',
|
||||||
|
validateParams(z.object({ id: z.string().uuid() })),
|
||||||
|
validateBody(z.object({ emailPassword: z.string().min(1) })),
|
||||||
|
sensitiveOperationLimiter,
|
||||||
|
emailAccountController.updateEmailAccountPassword
|
||||||
|
);
|
||||||
|
|
||||||
|
// Toggle email account status
|
||||||
|
router.patch(
|
||||||
|
'/:id/status',
|
||||||
|
validateParams(z.object({ id: z.string().uuid() })),
|
||||||
|
validateBody(z.object({ isActive: z.boolean() })),
|
||||||
|
emailAccountController.toggleEmailAccountStatus
|
||||||
|
);
|
||||||
|
|
||||||
|
// Set email account as primary
|
||||||
|
router.post(
|
||||||
|
'/:id/set-primary',
|
||||||
|
validateParams(z.object({ id: z.string().uuid() })),
|
||||||
|
emailAccountController.setPrimaryEmailAccount
|
||||||
|
);
|
||||||
|
|
||||||
|
// Delete email account
|
||||||
|
router.delete(
|
||||||
|
'/:id',
|
||||||
|
validateParams(z.object({ id: z.string().uuid() })),
|
||||||
|
sensitiveOperationLimiter,
|
||||||
|
emailAccountController.deleteEmailAccount
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -6,12 +6,19 @@ import { syncEmailsFromSender } from './jmap.service.js';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all contacts for a user
|
* Get all contacts for a user
|
||||||
|
* If emailAccountId is provided, filter by that account, otherwise return all
|
||||||
*/
|
*/
|
||||||
export const getUserContacts = async (userId) => {
|
export const getUserContacts = async (userId, emailAccountId = null) => {
|
||||||
|
const conditions = [eq(contacts.userId, userId)];
|
||||||
|
|
||||||
|
if (emailAccountId) {
|
||||||
|
conditions.push(eq(contacts.emailAccountId, emailAccountId));
|
||||||
|
}
|
||||||
|
|
||||||
const userContacts = await db
|
const userContacts = await db
|
||||||
.select()
|
.select()
|
||||||
.from(contacts)
|
.from(contacts)
|
||||||
.where(eq(contacts.userId, userId))
|
.where(and(...conditions))
|
||||||
.orderBy(desc(contacts.addedAt));
|
.orderBy(desc(contacts.addedAt));
|
||||||
|
|
||||||
return userContacts;
|
return userContacts;
|
||||||
@@ -20,12 +27,18 @@ export const getUserContacts = async (userId) => {
|
|||||||
/**
|
/**
|
||||||
* Add a new contact and sync their emails
|
* Add a new contact and sync their emails
|
||||||
*/
|
*/
|
||||||
export const addContact = async (userId, jmapConfig, email, name = '', notes = '') => {
|
export const addContact = async (userId, emailAccountId, jmapConfig, email, name = '', notes = '') => {
|
||||||
// Check if contact already exists
|
// Check if contact already exists for this email account
|
||||||
const [existing] = await db
|
const [existing] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(contacts)
|
.from(contacts)
|
||||||
.where(and(eq(contacts.userId, userId), eq(contacts.email, email)))
|
.where(
|
||||||
|
and(
|
||||||
|
eq(contacts.userId, userId),
|
||||||
|
eq(contacts.emailAccountId, emailAccountId),
|
||||||
|
eq(contacts.email, email)
|
||||||
|
)
|
||||||
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -37,6 +50,7 @@ export const addContact = async (userId, jmapConfig, email, name = '', notes = '
|
|||||||
.insert(contacts)
|
.insert(contacts)
|
||||||
.values({
|
.values({
|
||||||
userId,
|
userId,
|
||||||
|
emailAccountId,
|
||||||
email,
|
email,
|
||||||
name: name || email.split('@')[0],
|
name: name || email.split('@')[0],
|
||||||
notes: notes || null,
|
notes: notes || null,
|
||||||
@@ -45,7 +59,7 @@ export const addContact = async (userId, jmapConfig, email, name = '', notes = '
|
|||||||
|
|
||||||
// Sync emails from this sender
|
// Sync emails from this sender
|
||||||
try {
|
try {
|
||||||
await syncEmailsFromSender(jmapConfig, userId, newContact.id, email);
|
await syncEmailsFromSender(jmapConfig, userId, emailAccountId, newContact.id, email);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to sync emails for new contact:', error);
|
console.error('Failed to sync emails for new contact:', error);
|
||||||
// Don't throw - contact was created successfully
|
// Don't throw - contact was created successfully
|
||||||
|
|||||||
@@ -5,8 +5,15 @@ import { NotFoundError } from '../utils/errors.js';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all emails for a user (only from added contacts)
|
* Get all emails for a user (only from added contacts)
|
||||||
|
* If emailAccountId is provided, filter by that account
|
||||||
*/
|
*/
|
||||||
export const getUserEmails = async (userId) => {
|
export const getUserEmails = async (userId, emailAccountId = null) => {
|
||||||
|
const conditions = [eq(emails.userId, userId)];
|
||||||
|
|
||||||
|
if (emailAccountId) {
|
||||||
|
conditions.push(eq(emails.emailAccountId, emailAccountId));
|
||||||
|
}
|
||||||
|
|
||||||
const userEmails = await db
|
const userEmails = await db
|
||||||
.select({
|
.select({
|
||||||
id: emails.id,
|
id: emails.id,
|
||||||
@@ -21,6 +28,7 @@ export const getUserEmails = async (userId) => {
|
|||||||
isRead: emails.isRead,
|
isRead: emails.isRead,
|
||||||
date: emails.date,
|
date: emails.date,
|
||||||
createdAt: emails.createdAt,
|
createdAt: emails.createdAt,
|
||||||
|
emailAccountId: emails.emailAccountId,
|
||||||
contact: {
|
contact: {
|
||||||
id: contacts.id,
|
id: contacts.id,
|
||||||
email: contacts.email,
|
email: contacts.email,
|
||||||
@@ -29,7 +37,7 @@ export const getUserEmails = async (userId) => {
|
|||||||
})
|
})
|
||||||
.from(emails)
|
.from(emails)
|
||||||
.leftJoin(contacts, eq(emails.contactId, contacts.id))
|
.leftJoin(contacts, eq(emails.contactId, contacts.id))
|
||||||
.where(eq(emails.userId, userId))
|
.where(and(...conditions))
|
||||||
.orderBy(desc(emails.date));
|
.orderBy(desc(emails.date));
|
||||||
|
|
||||||
return userEmails;
|
return userEmails;
|
||||||
@@ -54,27 +62,31 @@ export const getEmailThread = async (userId, threadId) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Search emails (from, to, subject)
|
* Search emails (from, to, subject)
|
||||||
|
* If emailAccountId is provided, filter by that account
|
||||||
*/
|
*/
|
||||||
export const searchEmails = async (userId, query) => {
|
export const searchEmails = async (userId, query, emailAccountId = null) => {
|
||||||
if (!query || query.trim().length < 2) {
|
if (!query || query.trim().length < 2) {
|
||||||
throw new Error('Search term must be at least 2 characters');
|
throw new Error('Search term must be at least 2 characters');
|
||||||
}
|
}
|
||||||
|
|
||||||
const searchPattern = `%${query}%`;
|
const searchPattern = `%${query}%`;
|
||||||
|
const conditions = [
|
||||||
const results = await db
|
|
||||||
.select()
|
|
||||||
.from(emails)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(emails.userId, userId),
|
eq(emails.userId, userId),
|
||||||
or(
|
or(
|
||||||
like(emails.from, searchPattern),
|
like(emails.from, searchPattern),
|
||||||
like(emails.to, searchPattern),
|
like(emails.to, searchPattern),
|
||||||
like(emails.subject, searchPattern)
|
like(emails.subject, searchPattern)
|
||||||
)
|
),
|
||||||
)
|
];
|
||||||
)
|
|
||||||
|
if (emailAccountId) {
|
||||||
|
conditions.push(eq(emails.emailAccountId, emailAccountId));
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await db
|
||||||
|
.select()
|
||||||
|
.from(emails)
|
||||||
|
.where(and(...conditions))
|
||||||
.orderBy(desc(emails.date))
|
.orderBy(desc(emails.date))
|
||||||
.limit(50);
|
.limit(50);
|
||||||
|
|
||||||
@@ -83,14 +95,34 @@ export const searchEmails = async (userId, query) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get unread email count
|
* Get unread email count
|
||||||
|
* Returns total count and counts per email account
|
||||||
*/
|
*/
|
||||||
export const getUnreadCount = async (userId) => {
|
export const getUnreadCount = async (userId) => {
|
||||||
const result = await db
|
// Get total unread count
|
||||||
|
const totalResult = await db
|
||||||
.select({ count: sql`count(*)::int` })
|
.select({ count: sql`count(*)::int` })
|
||||||
.from(emails)
|
.from(emails)
|
||||||
.where(and(eq(emails.userId, userId), eq(emails.isRead, false)));
|
.where(and(eq(emails.userId, userId), eq(emails.isRead, false)));
|
||||||
|
|
||||||
return result[0]?.count || 0;
|
const totalUnread = totalResult[0]?.count || 0;
|
||||||
|
|
||||||
|
// Get unread count per email account
|
||||||
|
const accountCounts = await db
|
||||||
|
.select({
|
||||||
|
emailAccountId: emails.emailAccountId,
|
||||||
|
count: sql`count(*)::int`,
|
||||||
|
})
|
||||||
|
.from(emails)
|
||||||
|
.where(and(eq(emails.userId, userId), eq(emails.isRead, false)))
|
||||||
|
.groupBy(emails.emailAccountId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalUnread,
|
||||||
|
accounts: accountCounts.map((ac) => ({
|
||||||
|
emailAccountId: ac.emailAccountId,
|
||||||
|
unreadCount: ac.count,
|
||||||
|
})),
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
282
src/services/email-account.service.js
Normal file
282
src/services/email-account.service.js
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
import { eq, and } from 'drizzle-orm';
|
||||||
|
import { db } from '../config/database.js';
|
||||||
|
import { emailAccounts, users } from '../db/schema.js';
|
||||||
|
import { encryptPassword, decryptPassword } from '../utils/password.js';
|
||||||
|
import { validateJmapCredentials } from './email.service.js';
|
||||||
|
import {
|
||||||
|
NotFoundError,
|
||||||
|
ValidationError,
|
||||||
|
ConflictError,
|
||||||
|
AuthenticationError,
|
||||||
|
} from '../utils/errors.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all email accounts for a user
|
||||||
|
*/
|
||||||
|
export const getUserEmailAccounts = async (userId) => {
|
||||||
|
const accounts = await db
|
||||||
|
.select({
|
||||||
|
id: emailAccounts.id,
|
||||||
|
userId: emailAccounts.userId,
|
||||||
|
email: emailAccounts.email,
|
||||||
|
jmapAccountId: emailAccounts.jmapAccountId,
|
||||||
|
isPrimary: emailAccounts.isPrimary,
|
||||||
|
isActive: emailAccounts.isActive,
|
||||||
|
createdAt: emailAccounts.createdAt,
|
||||||
|
updatedAt: emailAccounts.updatedAt,
|
||||||
|
})
|
||||||
|
.from(emailAccounts)
|
||||||
|
.where(eq(emailAccounts.userId, userId))
|
||||||
|
.orderBy(emailAccounts.isPrimary, emailAccounts.createdAt);
|
||||||
|
|
||||||
|
return accounts;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific email account by ID
|
||||||
|
*/
|
||||||
|
export const getEmailAccountById = async (accountId, userId) => {
|
||||||
|
const [account] = await db
|
||||||
|
.select()
|
||||||
|
.from(emailAccounts)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(emailAccounts.id, accountId),
|
||||||
|
eq(emailAccounts.userId, userId)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
throw new NotFoundError('Email účet nenájdený');
|
||||||
|
}
|
||||||
|
|
||||||
|
return account;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get user's primary email account
|
||||||
|
*/
|
||||||
|
export const getPrimaryEmailAccount = async (userId) => {
|
||||||
|
const [account] = await db
|
||||||
|
.select()
|
||||||
|
.from(emailAccounts)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(emailAccounts.userId, userId),
|
||||||
|
eq(emailAccounts.isPrimary, true)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
return account || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new email account with JMAP validation
|
||||||
|
*/
|
||||||
|
export const createEmailAccount = async (userId, email, emailPassword) => {
|
||||||
|
// Check if email already exists for this user
|
||||||
|
const [existing] = await db
|
||||||
|
.select()
|
||||||
|
.from(emailAccounts)
|
||||||
|
.where(
|
||||||
|
and(
|
||||||
|
eq(emailAccounts.userId, userId),
|
||||||
|
eq(emailAccounts.email, email)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (existing) {
|
||||||
|
throw new ConflictError('Tento email účet už je pripojený');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate JMAP credentials and get account ID
|
||||||
|
let jmapAccountId;
|
||||||
|
try {
|
||||||
|
const validation = await validateJmapCredentials(email, emailPassword);
|
||||||
|
jmapAccountId = validation.accountId;
|
||||||
|
} catch (error) {
|
||||||
|
throw new AuthenticationError(
|
||||||
|
'Nepodarilo sa pripojiť k emailovému účtu. Skontrolujte prihlasovacie údaje.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt password
|
||||||
|
const encryptedPassword = encryptPassword(emailPassword);
|
||||||
|
|
||||||
|
// Check if this is the first email account for this user
|
||||||
|
const existingAccounts = await getUserEmailAccounts(userId);
|
||||||
|
const isFirst = existingAccounts.length === 0;
|
||||||
|
|
||||||
|
// Create email account
|
||||||
|
const [newAccount] = await db
|
||||||
|
.insert(emailAccounts)
|
||||||
|
.values({
|
||||||
|
userId,
|
||||||
|
email,
|
||||||
|
emailPassword: encryptedPassword,
|
||||||
|
jmapAccountId,
|
||||||
|
isPrimary: isFirst, // First account is automatically primary
|
||||||
|
isActive: true,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: newAccount.id,
|
||||||
|
email: newAccount.email,
|
||||||
|
jmapAccountId: newAccount.jmapAccountId,
|
||||||
|
isPrimary: newAccount.isPrimary,
|
||||||
|
isActive: newAccount.isActive,
|
||||||
|
createdAt: newAccount.createdAt,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update email account password
|
||||||
|
*/
|
||||||
|
export const updateEmailAccountPassword = async (accountId, userId, newPassword) => {
|
||||||
|
const account = await getEmailAccountById(accountId, userId);
|
||||||
|
|
||||||
|
// Validate new JMAP credentials
|
||||||
|
try {
|
||||||
|
await validateJmapCredentials(account.email, newPassword);
|
||||||
|
} catch (error) {
|
||||||
|
throw new AuthenticationError(
|
||||||
|
'Nepodarilo sa overiť nové heslo. Skontrolujte prihlasovacie údaje.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt password
|
||||||
|
const encryptedPassword = encryptPassword(newPassword);
|
||||||
|
|
||||||
|
// Update password
|
||||||
|
const [updated] = await db
|
||||||
|
.update(emailAccounts)
|
||||||
|
.set({
|
||||||
|
emailPassword: encryptedPassword,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(emailAccounts.id, accountId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: updated.id,
|
||||||
|
email: updated.email,
|
||||||
|
updatedAt: updated.updatedAt,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle email account active status
|
||||||
|
*/
|
||||||
|
export const toggleEmailAccountStatus = async (accountId, userId, isActive) => {
|
||||||
|
const account = await getEmailAccountById(accountId, userId);
|
||||||
|
|
||||||
|
// Cannot deactivate primary account
|
||||||
|
if (account.isPrimary && !isActive) {
|
||||||
|
throw new ValidationError('Nemôžete deaktivovať primárny email účet');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [updated] = await db
|
||||||
|
.update(emailAccounts)
|
||||||
|
.set({
|
||||||
|
isActive,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(emailAccounts.id, accountId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: updated.id,
|
||||||
|
isActive: updated.isActive,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set an email account as primary
|
||||||
|
*/
|
||||||
|
export const setPrimaryEmailAccount = async (accountId, userId) => {
|
||||||
|
const account = await getEmailAccountById(accountId, userId);
|
||||||
|
|
||||||
|
// Remove primary flag from all accounts
|
||||||
|
await db
|
||||||
|
.update(emailAccounts)
|
||||||
|
.set({ isPrimary: false, updatedAt: new Date() })
|
||||||
|
.where(eq(emailAccounts.userId, userId));
|
||||||
|
|
||||||
|
// Set new primary account
|
||||||
|
const [updated] = await db
|
||||||
|
.update(emailAccounts)
|
||||||
|
.set({
|
||||||
|
isPrimary: true,
|
||||||
|
isActive: true, // Primary account must be active
|
||||||
|
updatedAt: new Date(),
|
||||||
|
})
|
||||||
|
.where(eq(emailAccounts.id, accountId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: updated.id,
|
||||||
|
email: updated.email,
|
||||||
|
isPrimary: updated.isPrimary,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete an email account
|
||||||
|
* NOTE: This will cascade delete all associated contacts and emails
|
||||||
|
*/
|
||||||
|
export const deleteEmailAccount = async (accountId, userId) => {
|
||||||
|
const account = await getEmailAccountById(accountId, userId);
|
||||||
|
|
||||||
|
// Cannot delete primary account if it's the only one
|
||||||
|
if (account.isPrimary) {
|
||||||
|
const allAccounts = await getUserEmailAccounts(userId);
|
||||||
|
if (allAccounts.length === 1) {
|
||||||
|
throw new ValidationError('Nemôžete zmazať posledný email účet');
|
||||||
|
}
|
||||||
|
|
||||||
|
// If deleting primary account, make another account primary
|
||||||
|
const otherAccount = allAccounts.find(acc => acc.id !== accountId);
|
||||||
|
if (otherAccount) {
|
||||||
|
await setPrimaryEmailAccount(otherAccount.id, userId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete account (will cascade to contacts and emails)
|
||||||
|
await db
|
||||||
|
.delete(emailAccounts)
|
||||||
|
.where(eq(emailAccounts.id, accountId));
|
||||||
|
|
||||||
|
return {
|
||||||
|
message: 'Email účet bol úspešne odstránený',
|
||||||
|
deletedAccountId: accountId,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get email account with decrypted password (for JMAP operations)
|
||||||
|
*/
|
||||||
|
export const getEmailAccountWithCredentials = async (accountId, userId) => {
|
||||||
|
console.log('🔐 getEmailAccountWithCredentials called:', { accountId, userId });
|
||||||
|
const account = await getEmailAccountById(accountId, userId);
|
||||||
|
console.log('📦 Account retrieved:', {
|
||||||
|
id: account.id,
|
||||||
|
email: account.email,
|
||||||
|
hasPassword: !!account.emailPassword,
|
||||||
|
passwordLength: account.emailPassword?.length
|
||||||
|
});
|
||||||
|
|
||||||
|
const decryptedPassword = decryptPassword(account.emailPassword);
|
||||||
|
console.log('🔓 Password decrypted, length:', decryptedPassword?.length);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: account.id,
|
||||||
|
email: account.email,
|
||||||
|
emailPassword: decryptedPassword,
|
||||||
|
jmapAccountId: account.jmapAccountId,
|
||||||
|
isActive: account.isActive,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -11,7 +11,7 @@ import { decryptPassword } from '../utils/password.js';
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get JMAP configuration for user
|
* Get JMAP configuration for user (legacy - for backward compatibility)
|
||||||
*/
|
*/
|
||||||
export const getJmapConfig = (user) => {
|
export const getJmapConfig = (user) => {
|
||||||
if (!user.email || !user.emailPassword || !user.jmapAccountId) {
|
if (!user.email || !user.emailPassword || !user.jmapAccountId) {
|
||||||
@@ -29,6 +29,24 @@ export const getJmapConfig = (user) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get JMAP configuration from email account object
|
||||||
|
* NOTE: Expects emailPassword to be already decrypted (from getEmailAccountWithCredentials)
|
||||||
|
*/
|
||||||
|
export const getJmapConfigFromAccount = (emailAccount) => {
|
||||||
|
if (!emailAccount.email || !emailAccount.emailPassword || !emailAccount.jmapAccountId) {
|
||||||
|
throw new Error('Email account je neúplný');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Password is already decrypted by getEmailAccountWithCredentials
|
||||||
|
return {
|
||||||
|
server: process.env.JMAP_SERVER || 'https://mail.truemail.sk/jmap/',
|
||||||
|
username: emailAccount.email,
|
||||||
|
password: emailAccount.emailPassword,
|
||||||
|
accountId: emailAccount.jmapAccountId,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Make JMAP API request
|
* Make JMAP API request
|
||||||
*/
|
*/
|
||||||
@@ -329,6 +347,7 @@ export const searchEmailsJMAP = async (jmapConfig, userId, query, limit = 50, of
|
|||||||
export const syncEmailsFromSender = async (
|
export const syncEmailsFromSender = async (
|
||||||
jmapConfig,
|
jmapConfig,
|
||||||
userId,
|
userId,
|
||||||
|
emailAccountId,
|
||||||
contactId,
|
contactId,
|
||||||
senderEmail,
|
senderEmail,
|
||||||
options = {}
|
options = {}
|
||||||
@@ -336,7 +355,7 @@ export const syncEmailsFromSender = async (
|
|||||||
const { limit = 500 } = options;
|
const { limit = 500 } = options;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
logger.info(`Syncing emails from sender: ${senderEmail}`);
|
logger.info(`Syncing emails from sender: ${senderEmail} for account ${emailAccountId}`);
|
||||||
|
|
||||||
// Query all emails from this sender
|
// Query all emails from this sender
|
||||||
const queryResponse = await jmapRequest(jmapConfig, [
|
const queryResponse = await jmapRequest(jmapConfig, [
|
||||||
@@ -402,6 +421,12 @@ export const syncEmailsFromSender = async (
|
|||||||
const inReplyTo = Array.isArray(email.inReplyTo) ? email.inReplyTo[0] : email.inReplyTo;
|
const inReplyTo = Array.isArray(email.inReplyTo) ? email.inReplyTo[0] : email.inReplyTo;
|
||||||
const isRead = email.keywords && email.keywords['$seen'] === true;
|
const isRead = email.keywords && email.keywords['$seen'] === true;
|
||||||
|
|
||||||
|
// Skip emails without from or to (malformed data)
|
||||||
|
if (!fromEmail && !toEmail) {
|
||||||
|
logger.warn(`Skipping email ${messageId} - missing both from and to fields`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Skip if already exists
|
// Skip if already exists
|
||||||
const [existing] = await db
|
const [existing] = await db
|
||||||
.select()
|
.select()
|
||||||
@@ -416,6 +441,7 @@ export const syncEmailsFromSender = async (
|
|||||||
// Save email
|
// Save email
|
||||||
await db.insert(emails).values({
|
await db.insert(emails).values({
|
||||||
userId,
|
userId,
|
||||||
|
emailAccountId,
|
||||||
contactId,
|
contactId,
|
||||||
jmapId: email.id,
|
jmapId: email.id,
|
||||||
messageId,
|
messageId,
|
||||||
@@ -527,7 +553,7 @@ export const markEmailAsRead = async (jmapConfig, userId, jmapId, isRead) => {
|
|||||||
/**
|
/**
|
||||||
* Send email via JMAP
|
* Send email via JMAP
|
||||||
*/
|
*/
|
||||||
export const sendEmail = async (jmapConfig, userId, to, subject, body, inReplyTo = null, threadId = null) => {
|
export const sendEmail = async (jmapConfig, userId, emailAccountId, to, subject, body, inReplyTo = null, threadId = null) => {
|
||||||
try {
|
try {
|
||||||
logger.info(`Sending email to: ${to}`);
|
logger.info(`Sending email to: ${to}`);
|
||||||
|
|
||||||
@@ -615,6 +641,7 @@ export const sendEmail = async (jmapConfig, userId, to, subject, body, inReplyTo
|
|||||||
|
|
||||||
await db.insert(emails).values({
|
await db.insert(emails).values({
|
||||||
userId,
|
userId,
|
||||||
|
emailAccountId,
|
||||||
contactId: null, // Will be linked later if recipient is a contact
|
contactId: null, // Will be linked later if recipient is a contact
|
||||||
jmapId: createdEmailId,
|
jmapId: createdEmailId,
|
||||||
messageId,
|
messageId,
|
||||||
|
|||||||
27
src/validators/email-account.validators.js
Normal file
27
src/validators/email-account.validators.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
// Create email account schema
|
||||||
|
export const createEmailAccountSchema = z.object({
|
||||||
|
email: z
|
||||||
|
.string({
|
||||||
|
required_error: 'Email je povinný',
|
||||||
|
})
|
||||||
|
.email('Neplatný formát emailu')
|
||||||
|
.max(255, 'Email môže mať maximálne 255 znakov'),
|
||||||
|
emailPassword: z
|
||||||
|
.string({
|
||||||
|
required_error: 'Heslo k emailu je povinné',
|
||||||
|
})
|
||||||
|
.min(1, 'Heslo k emailu nemôže byť prázdne'),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update email account schema
|
||||||
|
export const updateEmailAccountSchema = z.object({
|
||||||
|
emailPassword: z.string().min(1, 'Heslo k emailu nemôže byť prázdne').optional(),
|
||||||
|
isActive: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set primary account schema
|
||||||
|
export const setPrimaryAccountSchema = z.object({
|
||||||
|
accountId: z.string().uuid('Neplatný formát account ID'),
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user