Files
emailToChat/src/main/ipc.ts
T
luzadevandClaude Opus 4.7 620b8fca67 Initial commit: EmailToChat — multi-channel email manager with AI
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>
2026-05-20 14:42:12 +02:00

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
})
}