Desktop app (Electron + Vite + React + TypeScript) that receives email via IMAP, sends/forwards via SMTP, Telegram (Bot API + MTProto user account) and WhatsApp (whatsapp-web.js), with AI integration (Anthropic, OpenAI, Ollama) for auto-classification, summary, structured extraction and reply drafts. Features: - Multi-account IMAP/SMTP with Gmail OAuth2 (loopback flow) + App Password preset - Telegram Bot API via telegraf, with chat discovery from getUpdates - Telegram MTProto via gram.js for sending to phone numbers like WhatsApp - WhatsApp via whatsapp-web.js with QR-code session (locally generated PNG) - Forwarding engine with rule-based filters incl. AI category match - SQLite persistence (better-sqlite3) with idempotent migrations - Contact picker with autocomplete, CSV/vCard import, search - Dark mode (light/dark/system) with futuristic glass-morphism UI - Settings tabbed: Email Accounts / Telegram / WhatsApp / AI / OAuth creds / Preferences Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
343 lines
12 KiB
TypeScript
343 lines
12 KiB
TypeScript
import { ipcMain, BrowserWindow } from 'electron'
|
|
import { IPC } from '@shared/ipc-channels'
|
|
import {
|
|
accountsRepo,
|
|
telegramRepo,
|
|
telegramUserRepo,
|
|
whatsappRepo,
|
|
messagesRepo,
|
|
rulesRepo,
|
|
contactsRepo,
|
|
aiProvidersRepo
|
|
} from './db/repositories'
|
|
import { settingsRepo, SETTING_KEYS } from './db/settings'
|
|
import { imapService } from './services/imap'
|
|
import { smtpService } from './services/smtp'
|
|
import { telegramService } from './services/telegram'
|
|
import { telegramMtprotoService } from './services/telegram-mtproto'
|
|
import { whatsappService } from './services/whatsapp'
|
|
import { aiService } from './services/ai'
|
|
import { connectGmail } from './services/gmail-oauth'
|
|
import { importContactsDialog } from './services/contacts-import'
|
|
import type {
|
|
IpcResponse,
|
|
SendRequest,
|
|
AiProviderConfig,
|
|
AppSettings,
|
|
EmailAccount
|
|
} from '@shared/types'
|
|
|
|
function ok<T>(data: T): IpcResponse<T> {
|
|
return { ok: true, data }
|
|
}
|
|
function fail(err: unknown): IpcResponse<never> {
|
|
return { ok: false, error: err instanceof Error ? err.message : String(err) }
|
|
}
|
|
|
|
function on<T>(channel: string, handler: (...args: any[]) => Promise<T> | T): void {
|
|
ipcMain.handle(channel, async (_event, ...args) => {
|
|
try {
|
|
return ok(await handler(...args))
|
|
} catch (err) {
|
|
console.error(`[ipc:${channel}]`, err)
|
|
return fail(err)
|
|
}
|
|
})
|
|
}
|
|
|
|
export function broadcast(channel: string, payload: unknown): void {
|
|
for (const win of BrowserWindow.getAllWindows()) {
|
|
win.webContents.send(channel, payload)
|
|
}
|
|
}
|
|
|
|
export function registerIpcHandlers(): void {
|
|
// -------- Accounts --------
|
|
on(IPC.accountsList, () => accountsRepo.list())
|
|
on(IPC.accountsCreate, (payload) => {
|
|
const created = accountsRepo.create(payload)
|
|
if (created.enabled) imapService.startPolling(created)
|
|
return created
|
|
})
|
|
on(IPC.accountsUpdate, (id, payload) => {
|
|
const updated = accountsRepo.update(id, payload)
|
|
imapService.stopPolling(id)
|
|
smtpService.invalidate(id)
|
|
if (updated.enabled) imapService.startPolling(updated)
|
|
return updated
|
|
})
|
|
on(IPC.accountsDelete, (id) => {
|
|
imapService.stopPolling(id)
|
|
smtpService.invalidate(id)
|
|
accountsRepo.delete(id)
|
|
return true
|
|
})
|
|
on(IPC.accountsTest, async (id) => {
|
|
const a = accountsRepo.get(id)
|
|
if (!a) throw new Error('Account non trovato')
|
|
await imapService.testConnection(a)
|
|
await smtpService.verify(a)
|
|
return true
|
|
})
|
|
on(IPC.accountsStartPolling, (id) => {
|
|
const a = accountsRepo.get(id)
|
|
if (!a) throw new Error('Account non trovato')
|
|
imapService.startPolling(a)
|
|
return true
|
|
})
|
|
on(IPC.accountsStopPolling, (id) => {
|
|
imapService.stopPolling(id)
|
|
return true
|
|
})
|
|
|
|
// -------- Telegram --------
|
|
on(IPC.telegramList, () => telegramRepo.list())
|
|
on(IPC.telegramCreate, (payload) => telegramRepo.create(payload))
|
|
on(IPC.telegramUpdate, (id, payload) => {
|
|
const bot = telegramRepo.update(id, payload)
|
|
telegramService.invalidate(id)
|
|
return bot
|
|
})
|
|
on(IPC.telegramDelete, (id) => {
|
|
telegramService.invalidate(id)
|
|
telegramRepo.delete(id)
|
|
return true
|
|
})
|
|
on(IPC.telegramTest, async (id) => {
|
|
const bot = telegramRepo.get(id)
|
|
if (!bot) throw new Error('Bot non trovato')
|
|
return await telegramService.verify(bot)
|
|
})
|
|
on(IPC.telegramDiscover, async (id) => {
|
|
const bot = telegramRepo.get(id)
|
|
if (!bot) throw new Error('Bot non trovato')
|
|
return await telegramService.discoverChats(bot)
|
|
})
|
|
|
|
// -------- Telegram User (MTProto) --------
|
|
on(IPC.tguList, () => telegramUserRepo.list())
|
|
on(IPC.tguCreate, (payload) => telegramUserRepo.create(payload))
|
|
on(IPC.tguDelete, async (id) => {
|
|
await telegramMtprotoService.logout(id).catch(() => undefined)
|
|
telegramUserRepo.delete(id)
|
|
return true
|
|
})
|
|
on(IPC.tguStartLogin, async (id) => {
|
|
await telegramMtprotoService.startLogin(id)
|
|
return true
|
|
})
|
|
on(IPC.tguSubmitCode, async (id, code: string) => {
|
|
return await telegramMtprotoService.submitCode(id, code)
|
|
})
|
|
on(IPC.tguSubmit2fa, async (id, password: string) => {
|
|
return await telegramMtprotoService.submit2fa(id, password)
|
|
})
|
|
on(IPC.tguLogout, async (id) => {
|
|
await telegramMtprotoService.logout(id)
|
|
return true
|
|
})
|
|
on(IPC.tguResolve, async (id, recipient: string) => {
|
|
const account = telegramUserRepo.get(id)
|
|
if (!account) throw new Error('Account non trovato')
|
|
return await telegramMtprotoService.resolveContact(account, recipient)
|
|
})
|
|
|
|
// -------- WhatsApp --------
|
|
on(IPC.whatsappList, () => whatsappRepo.list())
|
|
on(IPC.whatsappCreate, (payload) => whatsappRepo.create(payload))
|
|
on(IPC.whatsappUpdate, (id, payload) => whatsappRepo.update(id, payload))
|
|
on(IPC.whatsappDelete, async (id) => {
|
|
await whatsappService.stop(id)
|
|
whatsappRepo.delete(id)
|
|
return true
|
|
})
|
|
on(IPC.whatsappStart, async (id) => {
|
|
const s = whatsappRepo.get(id)
|
|
if (!s) throw new Error('Sessione non trovata')
|
|
await whatsappService.start(s)
|
|
return true
|
|
})
|
|
on(IPC.whatsappStop, async (id) => {
|
|
await whatsappService.stop(id)
|
|
return true
|
|
})
|
|
on(IPC.whatsappStatus, (id) => whatsappService.getStatus(id))
|
|
|
|
// -------- Messages --------
|
|
on(IPC.messagesList, (filter) => messagesRepo.list(filter))
|
|
on(IPC.messagesGet, (id) => messagesRepo.get(id))
|
|
on(IPC.messagesDelete, (id) => {
|
|
messagesRepo.delete(id)
|
|
return true
|
|
})
|
|
on(IPC.messagesSend, async (req: SendRequest) => {
|
|
let result: { messageId: string | number }
|
|
if (req.channel === 'smtp') {
|
|
if (!req.accountId) throw new Error('accountId obbligatorio per SMTP')
|
|
const account = accountsRepo.get(req.accountId)
|
|
if (!account) throw new Error('Account non trovato')
|
|
result = await smtpService.send(account, {
|
|
to: req.to,
|
|
cc: req.cc,
|
|
bcc: req.bcc,
|
|
subject: req.subject,
|
|
text: req.body,
|
|
html: req.html,
|
|
attachments: req.attachments
|
|
})
|
|
} else if (req.channel === 'telegram') {
|
|
if (!req.accountId) throw new Error('Bot Telegram id obbligatorio (accountId)')
|
|
const bot = telegramRepo.get(req.accountId)
|
|
if (!bot) throw new Error('Bot non trovato')
|
|
const res = await telegramService.send(bot, req.to, req.body, req.attachments)
|
|
result = { messageId: res.messageId }
|
|
} else if (req.channel === 'telegram_user') {
|
|
if (!req.accountId) throw new Error('Account Telegram (MTProto) id obbligatorio')
|
|
const account = telegramUserRepo.get(req.accountId)
|
|
if (!account) throw new Error('Account Telegram non trovato')
|
|
const res = await telegramMtprotoService.send(account, req.to, req.body, req.attachments)
|
|
result = { messageId: res.messageId }
|
|
} else if (req.channel === 'whatsapp') {
|
|
if (!req.accountId) throw new Error('Sessione WhatsApp id obbligatorio (accountId)')
|
|
const res = await whatsappService.send(req.accountId, req.to, req.body, req.attachments)
|
|
result = res
|
|
} else {
|
|
throw new Error(`Canale non supportato: ${req.channel}`)
|
|
}
|
|
|
|
const stored = messagesRepo.create({
|
|
accountId: req.channel === 'smtp' ? req.accountId : undefined,
|
|
direction: 'outbound',
|
|
channel: req.channel,
|
|
externalId: String(result.messageId),
|
|
toAddr: req.to,
|
|
cc: req.cc,
|
|
bcc: req.bcc,
|
|
subject: req.subject,
|
|
bodyText: req.body,
|
|
bodyHtml: req.html,
|
|
status: 'sent',
|
|
sentAt: new Date().toISOString()
|
|
} as any)
|
|
return stored
|
|
})
|
|
|
|
// -------- Rules --------
|
|
on(IPC.rulesList, () => rulesRepo.list())
|
|
on(IPC.rulesCreate, (payload) => rulesRepo.create(payload))
|
|
on(IPC.rulesUpdate, (id, payload) => rulesRepo.update(id, payload))
|
|
on(IPC.rulesDelete, (id) => {
|
|
rulesRepo.delete(id)
|
|
return true
|
|
})
|
|
|
|
// -------- Contacts --------
|
|
on(IPC.contactsList, () => contactsRepo.list())
|
|
on(IPC.contactsCreate, (payload) => contactsRepo.create(payload))
|
|
on(IPC.contactsUpdate, (id, payload) => contactsRepo.update(id, payload))
|
|
on(IPC.contactsDelete, (id) => {
|
|
contactsRepo.delete(id)
|
|
return true
|
|
})
|
|
on(IPC.contactsImport, async () => {
|
|
const result = await importContactsDialog()
|
|
return result
|
|
})
|
|
|
|
// -------- AI --------
|
|
on(IPC.aiList, () => aiProvidersRepo.list())
|
|
on(IPC.aiCreate, (payload: Omit<AiProviderConfig, 'id'>) => aiProvidersRepo.create(payload))
|
|
on(IPC.aiUpdate, (id, payload) => aiProvidersRepo.update(id, payload))
|
|
on(IPC.aiDelete, (id) => {
|
|
aiProvidersRepo.delete(id)
|
|
return true
|
|
})
|
|
on(IPC.aiTest, async (id) => {
|
|
const p = aiProvidersRepo.get(id)
|
|
if (!p) throw new Error('Provider non trovato')
|
|
return await aiService.test(p)
|
|
})
|
|
on(IPC.aiSummarize, async (messageId, providerId) => {
|
|
const message = messagesRepo.get(messageId)
|
|
if (!message) throw new Error('Messaggio non trovato')
|
|
const provider = providerId ? aiProvidersRepo.get(providerId) : undefined
|
|
return await aiService.summarize(message, provider)
|
|
})
|
|
on(IPC.aiClassify, async (messageId, providerId) => {
|
|
const message = messagesRepo.get(messageId)
|
|
if (!message) throw new Error('Messaggio non trovato')
|
|
const provider = providerId ? aiProvidersRepo.get(providerId) : undefined
|
|
return await aiService.classify(message, provider)
|
|
})
|
|
on(IPC.aiExtract, async (messageId, providerId) => {
|
|
const message = messagesRepo.get(messageId)
|
|
if (!message) throw new Error('Messaggio non trovato')
|
|
const provider = providerId ? aiProvidersRepo.get(providerId) : undefined
|
|
return await aiService.extract(message, provider)
|
|
})
|
|
on(IPC.aiSuggestReply, async (messageId, providerId) => {
|
|
const message = messagesRepo.get(messageId)
|
|
if (!message) throw new Error('Messaggio non trovato')
|
|
const provider = providerId ? aiProvidersRepo.get(providerId) : undefined
|
|
return await aiService.suggestReply(message, provider)
|
|
})
|
|
|
|
// -------- Settings --------
|
|
on(IPC.settingsGet, (): AppSettings => ({
|
|
gmailClientId: settingsRepo.get(SETTING_KEYS.gmailClientId),
|
|
gmailClientSecret: settingsRepo.get(SETTING_KEYS.gmailClientSecret),
|
|
telegramApiId: settingsRepo.get(SETTING_KEYS.telegramApiId),
|
|
telegramApiHash: settingsRepo.get(SETTING_KEYS.telegramApiHash)
|
|
}))
|
|
on(IPC.settingsUpdate, (payload: AppSettings) => {
|
|
if ('gmailClientId' in payload) {
|
|
settingsRepo.set(SETTING_KEYS.gmailClientId, payload.gmailClientId ?? null)
|
|
}
|
|
if ('gmailClientSecret' in payload) {
|
|
settingsRepo.set(SETTING_KEYS.gmailClientSecret, payload.gmailClientSecret ?? null)
|
|
}
|
|
if ('telegramApiId' in payload) {
|
|
settingsRepo.set(SETTING_KEYS.telegramApiId, payload.telegramApiId ?? null)
|
|
}
|
|
if ('telegramApiHash' in payload) {
|
|
settingsRepo.set(SETTING_KEYS.telegramApiHash, payload.telegramApiHash ?? null)
|
|
}
|
|
return true
|
|
})
|
|
|
|
// -------- Gmail OAuth connect --------
|
|
on(IPC.gmailConnect, async (): Promise<EmailAccount> => {
|
|
const result = await connectGmail()
|
|
const account = accountsRepo.create({
|
|
name: result.name ? `${result.name} (Gmail)` : `Gmail: ${result.email}`,
|
|
authType: 'oauth_google',
|
|
imap: {
|
|
host: 'imap.gmail.com',
|
|
port: 993,
|
|
secure: true,
|
|
user: result.email,
|
|
pass: ''
|
|
},
|
|
smtp: {
|
|
host: 'smtp.gmail.com',
|
|
port: 465,
|
|
secure: true,
|
|
user: result.email,
|
|
pass: '',
|
|
fromName: result.name,
|
|
fromAddress: result.email
|
|
},
|
|
oauth: {
|
|
provider: 'google',
|
|
refreshToken: result.refreshToken,
|
|
accessToken: result.accessToken,
|
|
expiresAt: result.expiresAt
|
|
},
|
|
enabled: true,
|
|
pollInterval: 60
|
|
})
|
|
imapService.startPolling(account)
|
|
return account
|
|
})
|
|
}
|