diff --git a/src/main/java/com/seconectai/condocontab/Main.java b/src/main/java/com/seconectai/condocontab/Main.java index 709f877..7c58f88 100644 --- a/src/main/java/com/seconectai/condocontab/Main.java +++ b/src/main/java/com/seconectai/condocontab/Main.java @@ -54,7 +54,7 @@ public final class Main { AuthHandlers authHandlers = new AuthHandlers(config, dataSourceProvider, jwtService); AuthMiddleware authMiddleware = new AuthMiddleware(jwtService, apiKeyService); TenantDataSourceProvider tenantDataSourceProvider = new TenantDataSourceProvider(config, dataSourceProvider); - MasterHandlers masterHandlers = new MasterHandlers(dataSourceProvider, new TenantProvisioner(config), evolutionApiClient, config.jwtSecret()); + MasterHandlers masterHandlers = new MasterHandlers(dataSourceProvider, new TenantProvisioner(config), evolutionApiClient, config.jwtSecret(), emailClient, tenantDataSourceProvider); ClienteHandlers clienteHandlers = new ClienteHandlers(tenantDataSourceProvider, dataSourceProvider, Path.of("/app/uploads"), evolutionApiClient, emailClient); FinanceiroHandlers financeiroHandlers = new FinanceiroHandlers(dataSourceProvider, abacatePayClient); AbacatePayWebhookHandler abacatePayWebhookHandler = new AbacatePayWebhookHandler(dataSourceProvider, abacatePayClient); @@ -69,6 +69,17 @@ public final class Main { e.printStackTrace(System.err); } }, 2, 5, TimeUnit.SECONDS); + + Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(() -> { + try { + Map cfg = clienteHandlers.carregarConfigEmail(); + int sync = emailClient.sincronizarRespostasEmail(cfg, tenantDataSourceProvider, dataSourceProvider); + if (sync > 0) System.out.println("Sincronizadas " + sync + " respostas de e-mail"); + } catch (Exception e) { + System.err.println("Erro na sincronização periódica de e-mails: " + e.getMessage()); + } + }, 10, 60, TimeUnit.SECONDS); + System.out.println("Mensageiro de envios iniciado"); new CountDownLatch(1).await(); return; diff --git a/src/main/java/com/seconectai/condocontab/integracao/EmailClient.java b/src/main/java/com/seconectai/condocontab/integracao/EmailClient.java index 3c0b7bd..9fe830f 100644 --- a/src/main/java/com/seconectai/condocontab/integracao/EmailClient.java +++ b/src/main/java/com/seconectai/condocontab/integracao/EmailClient.java @@ -1,36 +1,352 @@ package com.seconectai.condocontab.integracao; -import jakarta.mail.Message; -import jakarta.mail.MessagingException; -import jakarta.mail.PasswordAuthentication; -import jakarta.mail.Session; -import jakarta.mail.Transport; +import com.seconectai.condocontab.config.DataSourceProvider; +import com.seconectai.condocontab.tenant.TenantDataSourceProvider; +import com.seconectai.condocontab.util.Json; +import jakarta.mail.*; import jakarta.mail.internet.InternetAddress; import jakarta.mail.internet.MimeMessage; -import java.util.Map; -import java.util.Properties; +import java.sql.*; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.*; public final class EmailClient { + public void enviar(Map smtp, String destino, String assunto, String texto) throws MessagingException { if (smtp == null || smtp.getOrDefault("smtp_host", "").isBlank()) return; - Properties props = new Properties(); - props.put("mail.smtp.host", smtp.get("smtp_host")); - props.put("mail.smtp.port", smtp.getOrDefault("smtp_port", "587")); - props.put("mail.smtp.auth", "true"); - props.put("mail.smtp.starttls.enable", smtp.getOrDefault("smtp_starttls", "true")); + Properties props = buildSmtpProperties(smtp); String user = smtp.getOrDefault("smtp_user", ""); String pass = smtp.getOrDefault("smtp_password", ""); - Session session = Session.getInstance(props, new jakarta.mail.Authenticator() { - @Override protected PasswordAuthentication getPasswordAuthentication() { + + Session session = Session.getInstance(props, new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication(user, pass); } }); + MimeMessage message = new MimeMessage(session); - message.setFrom(new InternetAddress(smtp.getOrDefault("smtp_from", user))); + String from = smtp.getOrDefault("smtp_from", user); + if (from == null || from.isBlank()) from = user; + message.setFrom(new InternetAddress(from)); message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(destino)); - message.setSubject(assunto == null ? "Aviso" : assunto, "UTF-8"); + message.setSubject(assunto == null ? "Notificação" : assunto, "UTF-8"); message.setText(texto == null ? "" : texto, "UTF-8"); Transport.send(message); } + + public Map testarSmtp(Map smtp) { + if (smtp == null || smtp.getOrDefault("smtp_host", "").isBlank()) { + return Map.of("ok", false, "mensagem", "Servidor SMTP não configurado."); + } + try { + Properties props = buildSmtpProperties(smtp); + String user = smtp.getOrDefault("smtp_user", ""); + String pass = smtp.getOrDefault("smtp_password", ""); + String portStr = smtp.getOrDefault("smtp_port", "465").replaceAll("\\D", ""); + int port = portStr.isBlank() ? 465 : Integer.parseInt(portStr); + String host = smtp.get("smtp_host"); + + boolean isSsl = "ssl_tls".equalsIgnoreCase(smtp.getOrDefault("smtp_security", "ssl_tls")) || port == 465; + Session session = Session.getInstance(props, null); + Transport transport = session.getTransport(isSsl ? "smtps" : "smtp"); + try { + transport.connect(host, port, user, pass); + return Map.of("ok", true, "mensagem", "Conexão SMTP estabelecida com sucesso."); + } finally { + if (transport.isConnected()) { + transport.close(); + } + } + } catch (Exception e) { + return Map.of("ok", false, "mensagem", "Falha na conexão SMTP: " + (e.getMessage() != null ? e.getMessage() : e.toString())); + } + } + + public Map testarImap(Map imap) { + if (imap == null || imap.getOrDefault("imap_host", "").isBlank()) { + return Map.of("ok", false, "mensagem", "Servidor IMAP não configurado."); + } + try { + Properties props = buildImapProperties(imap); + String user = imap.getOrDefault("imap_user", ""); + String pass = imap.getOrDefault("imap_password", ""); + String portStr = imap.getOrDefault("imap_port", "993").replaceAll("\\D", ""); + int port = portStr.isBlank() ? 993 : Integer.parseInt(portStr); + String host = imap.get("imap_host"); + + boolean isSsl = "ssl_tls".equalsIgnoreCase(imap.getOrDefault("imap_security", "ssl_tls")) || port == 993; + Session session = Session.getInstance(props, null); + Store store = session.getStore(isSsl ? "imaps" : "imap"); + try { + store.connect(host, port, user, pass); + Folder inbox = store.getFolder("INBOX"); + inbox.open(Folder.READ_ONLY); + int count = inbox.getMessageCount(); + inbox.close(false); + return Map.of("ok", true, "mensagem", "Conexão IMAP estabelecida com sucesso (" + count + " mensagens na caixa de entrada)."); + } finally { + if (store.isConnected()) { + store.close(); + } + } + } catch (Exception e) { + return Map.of("ok", false, "mensagem", "Falha na conexão IMAP: " + (e.getMessage() != null ? e.getMessage() : e.toString())); + } + } + + public int sincronizarRespostasEmail(Map config, TenantDataSourceProvider tenantDs, DataSourceProvider masterDs) { + if (config == null || config.getOrDefault("imap_host", "").isBlank()) return 0; + int importados = 0; + try { + Properties props = buildImapProperties(config); + String user = config.getOrDefault("imap_user", ""); + String pass = config.getOrDefault("imap_password", ""); + if (user.isBlank() || pass.isBlank()) return 0; + + String portStr = config.getOrDefault("imap_port", "993").replaceAll("\\D", ""); + int port = portStr.isBlank() ? 993 : Integer.parseInt(portStr); + String host = config.get("imap_host"); + boolean isSsl = "ssl_tls".equalsIgnoreCase(config.getOrDefault("imap_security", "ssl_tls")) || port == 993; + + Session session = Session.getInstance(props, null); + Store store = session.getStore(isSsl ? "imaps" : "imap"); + try { + store.connect(host, port, user, pass); + Folder inbox = store.getFolder("INBOX"); + inbox.open(Folder.READ_ONLY); + + int total = inbox.getMessageCount(); + if (total <= 0) { + inbox.close(false); + return 0; + } + int start = Math.max(1, total - 49); + Message[] msgs = inbox.getMessages(start, total); + + List empresaIds = new ArrayList<>(); + try (Connection m = masterDs.getMasterConnection(); + PreparedStatement ps = m.prepareStatement("SELECT e.id FROM empresas e JOIN pg_database d ON d.datname=e.database_name WHERE e.status='ativa'")) { + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) empresaIds.add(rs.getLong(1)); + } + } + + for (Message msg : msgs) { + Address[] fromArr = msg.getFrom(); + if (fromArr == null || fromArr.length == 0) continue; + String senderEmail = ""; + String senderName = ""; + if (fromArr[0] instanceof InternetAddress ia) { + senderEmail = ia.getAddress() != null ? ia.getAddress().trim().toLowerCase() : ""; + senderName = ia.getPersonal() != null ? ia.getPersonal() : senderEmail; + } else { + senderEmail = fromArr[0].toString().trim().toLowerCase(); + senderName = senderEmail; + } + if (senderEmail.isBlank()) continue; + + String[] msgIdHeaders = msg.getHeader("Message-ID"); + String messageId = (msgIdHeaders != null && msgIdHeaders.length > 0) ? msgIdHeaders[0] : (""); + String subject = msg.getSubject() != null ? msg.getSubject() : ""; + String body = ""; + try { + body = getTextFromPart(msg); + } catch (Exception ignored) {} + if (body.length() > 4000) body = body.substring(0, 4000); + + Timestamp dataRecebida = msg.getSentDate() != null ? new Timestamp(msg.getSentDate().getTime()) : Timestamp.valueOf(LocalDateTime.now(ZoneId.of("America/Sao_Paulo"))); + + for (Long empId : empresaIds) { + try (Connection tc = tenantDs.getConnection(empId)) { + try (PreparedStatement checkPs = tc.prepareStatement("SELECT count(*) FROM respostas WHERE payload LIKE ?")) { + checkPs.setString(1, "%" + messageId + "%"); + try (ResultSet rs = checkPs.executeQuery()) { + if (rs.next() && rs.getInt(1) > 0) continue; + } + } + + Long condId = null; + Long contatoId = null; + String tipoOrigem = "email"; + String telOrigem = senderEmail; + String nomeOrigem = senderName; + + try (PreparedStatement cPs = tc.prepareStatement("SELECT c.id, c.nome, c.telefone, cc.condominio_id, cc.tipo FROM contatos c JOIN contato_condominio cc ON cc.contato_id=c.id WHERE lower(c.email)=? LIMIT 1")) { + cPs.setString(1, senderEmail); + try (ResultSet rs = cPs.executeQuery()) { + if (rs.next()) { + contatoId = rs.getLong("id"); + condId = rs.getLong("condominio_id"); + tipoOrigem = rs.getString("tipo"); + if (rs.getString("nome") != null && !rs.getString("nome").isBlank()) nomeOrigem = rs.getString("nome"); + if (rs.getString("telefone") != null && !rs.getString("telefone").isBlank()) telOrigem = rs.getString("telefone"); + } + } + } + + if (condId == null) { + try (PreparedStatement ePs = tc.prepareStatement("SELECT id, nome, telefone FROM destinatarios_extra WHERE lower(email)=? AND status<>'excluido' LIMIT 1")) { + ePs.setString(1, senderEmail); + try (ResultSet rs = ePs.executeQuery()) { + if (rs.next()) { + tipoOrigem = "extra"; + if (rs.getString("nome") != null && !rs.getString("nome").isBlank()) nomeOrigem = rs.getString("nome"); + if (rs.getString("telefone") != null && !rs.getString("telefone").isBlank()) telOrigem = rs.getString("telefone"); + try (PreparedStatement c1 = tc.prepareStatement("SELECT id FROM condominios ORDER BY id LIMIT 1"); + ResultSet r1 = c1.executeQuery()) { + if (r1.next()) condId = r1.getLong(1); + } + } + } + } + } + + if (condId != null) { + Map payloadMap = new LinkedHashMap<>(); + payloadMap.put("message_id", messageId); + payloadMap.put("subject", subject); + payloadMap.put("from", senderEmail); + payloadMap.put("date", dataRecebida.toString()); + String payloadJson = Json.write(payloadMap); + + try (PreparedStatement ins = tc.prepareStatement("INSERT INTO respostas (condominio_id, contato_id, tipo_origem, nome_origem, telefone_origem, resposta_texto, mensagem_original, payload, origem, status, recebido_em) VALUES (?,?,?,?,?,?,?,?,?,'recebida',?)")) { + ins.setLong(1, condId); + if (contatoId != null) ins.setLong(2, contatoId); else ins.setNull(2, Types.BIGINT); + ins.setString(3, tipoOrigem); + ins.setString(4, nomeOrigem); + ins.setString(5, telOrigem); + ins.setString(6, body.isBlank() ? subject : body); + ins.setString(7, subject); + ins.setString(8, payloadJson); + ins.setString(9, "email"); + ins.setTimestamp(10, dataRecebida); + ins.executeUpdate(); + importados++; + } + } + } catch (Exception err) { + System.err.println("Erro ao sincronizar email para empresa " + empId + ": " + err.getMessage()); + } + } + } + inbox.close(false); + } finally { + if (store.isConnected()) store.close(); + } + } catch (Exception e) { + System.err.println("Falha ao sincronizar respostas por e-mail via IMAP: " + e.getMessage()); + } + return importados; + } + + private String getTextFromPart(Part p) throws Exception { + if (p.isMimeType("text/plain")) { + Object content = p.getContent(); + return content != null ? content.toString() : ""; + } + if (p.isMimeType("text/html")) { + Object content = p.getContent(); + String html = content != null ? content.toString() : ""; + return html.replaceAll("<[^>]*>", " ").replaceAll("\\s+", " ").trim(); + } + if (p.isMimeType("multipart/*")) { + Multipart mp = (Multipart) p.getContent(); + StringBuilder sb = new StringBuilder(); + for (int i = 0; i < mp.getCount(); i++) { + BodyPart bp = mp.getBodyPart(i); + String sub = getTextFromPart(bp); + if (sub != null && !sub.isBlank()) sb.append(sub).append("\n"); + } + return sb.toString().trim(); + } + return ""; + } + + public static Properties buildSmtpProperties(Map smtp) { + Properties props = new Properties(); + String host = smtp.getOrDefault("smtp_host", ""); + String port = smtp.getOrDefault("smtp_port", "465"); + String security = smtp.getOrDefault("smtp_security", "").toLowerCase(); + boolean allowInsecure = "true".equalsIgnoreCase(smtp.get("smtp_allow_insecure_ssl")); + + props.put("mail.smtp.host", host); + props.put("mail.smtp.port", port); + props.put("mail.smtp.auth", "true"); + props.put("mail.smtp.connectiontimeout", "8000"); + props.put("mail.smtp.timeout", "8000"); + props.put("mail.smtp.writetimeout", "8000"); + + if (security.isBlank()) { + if ("465".equals(port)) security = "ssl_tls"; + else if ("true".equalsIgnoreCase(smtp.get("smtp_starttls"))) security = "starttls"; + else security = "ssl_tls"; + } + + if ("ssl_tls".equals(security)) { + props.put("mail.smtp.ssl.enable", "true"); + props.put("mail.smtp.socketFactory.port", port); + props.put("mail.smtp.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); + props.put("mail.smtp.socketFactory.fallback", "false"); + props.put("mail.smtps.ssl.enable", "true"); + props.put("mail.smtps.socketFactory.port", port); + props.put("mail.smtps.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); + props.put("mail.smtps.socketFactory.fallback", "false"); + } else if ("starttls".equals(security)) { + props.put("mail.smtp.starttls.enable", "true"); + props.put("mail.smtp.starttls.required", "true"); + } else { + props.put("mail.smtp.starttls.enable", "false"); + props.put("mail.smtp.ssl.enable", "false"); + } + + if (allowInsecure) { + props.put("mail.smtp.ssl.trust", "*"); + props.put("mail.smtps.ssl.trust", "*"); + props.put("mail.smtp.ssl.checkserveridentity", "false"); + props.put("mail.smtps.ssl.checkserveridentity", "false"); + } + return props; + } + + public static Properties buildImapProperties(Map imap) { + Properties props = new Properties(); + String host = imap.getOrDefault("imap_host", ""); + String port = imap.getOrDefault("imap_port", "993"); + String security = imap.getOrDefault("imap_security", "").toLowerCase(); + boolean allowInsecure = "true".equalsIgnoreCase(imap.get("imap_allow_insecure_ssl")); + + props.put("mail.imap.host", host); + props.put("mail.imap.port", port); + props.put("mail.imap.connectiontimeout", "8000"); + props.put("mail.imap.timeout", "8000"); + props.put("mail.imaps.connectiontimeout", "8000"); + props.put("mail.imaps.timeout", "8000"); + + if (security.isBlank()) { + security = "993".equals(port) ? "ssl_tls" : "starttls"; + } + + if ("ssl_tls".equals(security)) { + props.put("mail.imap.ssl.enable", "true"); + props.put("mail.imap.socketFactory.port", port); + props.put("mail.imap.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); + props.put("mail.imaps.ssl.enable", "true"); + props.put("mail.imaps.socketFactory.port", port); + props.put("mail.imaps.socketFactory.class", "javax.net.ssl.SSLSocketFactory"); + } else if ("starttls".equals(security)) { + props.put("mail.imap.starttls.enable", "true"); + props.put("mail.imap.starttls.required", "true"); + } + + if (allowInsecure) { + props.put("mail.imap.ssl.trust", "*"); + props.put("mail.imaps.ssl.trust", "*"); + props.put("mail.imap.ssl.checkserveridentity", "false"); + props.put("mail.imaps.ssl.checkserveridentity", "false"); + } + return props; + } } diff --git a/src/main/java/com/seconectai/condocontab/master/http/MasterHandlers.java b/src/main/java/com/seconectai/condocontab/master/http/MasterHandlers.java index 8ef94a2..2e211d1 100644 --- a/src/main/java/com/seconectai/condocontab/master/http/MasterHandlers.java +++ b/src/main/java/com/seconectai/condocontab/master/http/MasterHandlers.java @@ -4,11 +4,13 @@ import com.seconectai.condocontab.auth.PasswordHasher; import com.seconectai.condocontab.auth.ApiKeyService; import com.seconectai.condocontab.config.DataSourceProvider; import com.seconectai.condocontab.http.HttpResponses; +import com.seconectai.condocontab.integracao.EmailClient; import com.seconectai.condocontab.integracao.EvolutionApiClient; import com.seconectai.condocontab.master.dao.EmpresaDao; import com.seconectai.condocontab.master.dao.FuncionarioDao; import com.seconectai.condocontab.master.model.Empresa; import com.seconectai.condocontab.master.model.Funcionario; +import com.seconectai.condocontab.tenant.TenantDataSourceProvider; import com.seconectai.condocontab.tenant.provisioning.TenantProvisioner; import com.seconectai.condocontab.util.SimpleJson; import com.sun.net.httpserver.HttpExchange; @@ -31,12 +33,20 @@ public final class MasterHandlers implements HttpHandler { private final TenantProvisioner tenantProvisioner; private final EvolutionApiClient evolutionApiClient; private final String apiKeySecret; + private final EmailClient emailClient; + private final TenantDataSourceProvider tenantDs; - public MasterHandlers(DataSourceProvider dataSourceProvider, TenantProvisioner tenantProvisioner, EvolutionApiClient evolutionApiClient, String apiKeySecret) { + public MasterHandlers(DataSourceProvider dataSourceProvider, TenantProvisioner tenantProvisioner, EvolutionApiClient evolutionApiClient, String apiKeySecret, EmailClient emailClient, TenantDataSourceProvider tenantDs) { this.dataSourceProvider = dataSourceProvider; this.tenantProvisioner = tenantProvisioner; this.evolutionApiClient = evolutionApiClient; this.apiKeySecret = apiKeySecret; + this.emailClient = emailClient != null ? emailClient : new EmailClient(); + this.tenantDs = tenantDs; + } + + public MasterHandlers(DataSourceProvider dataSourceProvider, TenantProvisioner tenantProvisioner, EvolutionApiClient evolutionApiClient, String apiKeySecret) { + this(dataSourceProvider, tenantProvisioner, evolutionApiClient, apiKeySecret, new EmailClient(), null); } @Override @@ -139,6 +149,14 @@ public final class MasterHandlers implements HttpHandler { reiniciarEvolutionInstanciaPorNome(ex, parts[1]); return; } + if (parts.length == 2 && "configuracoes".equals(parts[0]) && "testar-email".equals(parts[1]) && "POST".equals(method)) { + testarEmail(ex); + return; + } + if (parts.length == 2 && "configuracoes".equals(parts[0]) && "sincronizar-email".equals(parts[1]) && "POST".equals(method)) { + sincronizarEmail(ex); + return; + } if (parts.length == 1 && "configuracoes".equals(parts[0])) { if ("GET".equals(method)) listarConfiguracoes(ex); else if ("PUT".equals(method)) salvarConfiguracoes(ex); @@ -679,7 +697,7 @@ public final class MasterHandlers implements HttpHandler { try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { String chave = rs.getString("chave"); - Object valor = Set.of("smtp_password", "abacatepay_api_key", "abacatepay_webhook_secret").contains(chave) ? "********" : rs.getString("valor"); + Object valor = Set.of("smtp_password", "imap_password", "abacatepay_api_key", "abacatepay_webhook_secret").contains(chave) ? "********" : rs.getString("valor"); cfg.put(chave, valor); Map row = new LinkedHashMap<>(); row.put("chave", chave); row.put("valor", valor); row.put("atualizado_em", rs.getObject("atualizado_em")); rows.add(row); } @@ -690,13 +708,17 @@ public final class MasterHandlers implements HttpHandler { private void salvarConfiguracoes(HttpExchange ex) throws Exception { Map b = body(ex); - Set permitidas = Set.of("smtp_host","smtp_port","smtp_user","smtp_password","smtp_from","smtp_starttls","abacatepay_api_key","abacatepay_webhook_secret"); + Set permitidas = Set.of( + "smtp_host","smtp_port","smtp_user","smtp_password","smtp_from","smtp_starttls","smtp_security","smtp_allow_insecure_ssl", + "imap_host","imap_port","imap_user","imap_password","imap_security","imap_allow_insecure_ssl", + "abacatepay_api_key","abacatepay_webhook_secret" + ); try (Connection conn = dataSourceProvider.getMasterConnection()) { try (PreparedStatement ps = conn.prepareStatement("INSERT INTO configuracoes_plataforma (chave, valor, atualizado_em) VALUES (?, ?, now()) ON CONFLICT (chave) DO UPDATE SET valor=EXCLUDED.valor, atualizado_em=now()")) { for (String chave : permitidas) { if (!b.containsKey(chave)) continue; String valor = b.get(chave); - if (Set.of("smtp_password", "abacatepay_api_key", "abacatepay_webhook_secret").contains(chave) && (valor == null || valor.isBlank() || "********".equals(valor))) continue; + if (Set.of("smtp_password", "imap_password", "abacatepay_api_key", "abacatepay_webhook_secret").contains(chave) && (valor == null || valor.isBlank() || "********".equals(valor))) continue; ps.setString(1, chave); ps.setString(2, valor); ps.addBatch(); } ps.executeBatch(); @@ -705,6 +727,40 @@ public final class MasterHandlers implements HttpHandler { listarConfiguracoes(ex); } + private void testarEmail(HttpExchange ex) throws Exception { + Map b = body(ex); + Map merged = carregarConfigEmailCompleta(); + for (Map.Entry entry : b.entrySet()) { + String val = entry.getValue(); + if (val != null && !val.isBlank() && !"********".equals(val)) { + merged.put(entry.getKey(), val); + } + } + Map smtpRes = emailClient.testarSmtp(merged); + Map imapRes = emailClient.testarImap(merged); + boolean ok = Boolean.TRUE.equals(smtpRes.get("ok")) && Boolean.TRUE.equals(imapRes.get("ok")); + HttpResponses.json(ex, 200, Map.of("ok", ok, "smtp", smtpRes, "imap", imapRes)); + } + + private void sincronizarEmail(HttpExchange ex) throws Exception { + Map cfg = carregarConfigEmailCompleta(); + int count = emailClient.sincronizarRespostasEmail(cfg, tenantDs, dataSourceProvider); + HttpResponses.json(ex, 200, Map.of("ok", true, "importados", count, "mensagem", count > 0 ? (count + " resposta(s) importada(s) com sucesso.") : "Nenhuma nova resposta encontrada na caixa de entrada.")); + } + + private Map carregarConfigEmailCompleta() throws SQLException { + try (Connection conn = dataSourceProvider.getMasterConnection(); + PreparedStatement ps = conn.prepareStatement("SELECT chave, valor FROM configuracoes_plataforma WHERE chave LIKE 'smtp_%' OR chave LIKE 'imap_%'")) { + Map map = new HashMap<>(); + try (ResultSet rs = ps.executeQuery()) { + while (rs.next()) { + map.put(rs.getString("chave"), rs.getString("valor")); + } + } + return map; + } + } + private void criarEmpresa(HttpExchange ex) throws Exception { Map body = body(ex); String nome = required(body, "nome"); diff --git a/src/main/java/com/seconectai/condocontab/tenant/http/ClienteHandlers.java b/src/main/java/com/seconectai/condocontab/tenant/http/ClienteHandlers.java index 1b0ca5f..9922e6a 100644 --- a/src/main/java/com/seconectai/condocontab/tenant/http/ClienteHandlers.java +++ b/src/main/java/com/seconectai/condocontab/tenant/http/ClienteHandlers.java @@ -579,7 +579,8 @@ public final class ClienteHandlers implements HttpHandler { private void logEnvio(long emp,long cond,long aviso,long contato,String nome,String canal,String status,String erro)throws SQLException{try(Connection m=masterDs.getMasterConnection();PreparedStatement ps=m.prepareStatement("INSERT INTO logs_envio_avisos (empresa_id,condominio_id,aviso_id,contato_id,contato_nome,canal,status,erro_detalhe) VALUES (?,?,?,?,?,?,?,?)")){ps.setLong(1,emp);ps.setLong(2,cond);ps.setLong(3,aviso);ps.setLong(4,contato);ps.setString(5,nome);ps.setString(6,canal);ps.setString(7,status);ps.setString(8,erro);ps.executeUpdate();}} private boolean funcionalidadeAtiva(long emp,String chave)throws SQLException{try(Connection m=masterDs.getMasterConnection();PreparedStatement ps=m.prepareStatement("SELECT COALESCE(ef.ativa,false) FROM funcionalidades f LEFT JOIN empresa_funcionalidades ef ON ef.funcionalidade_id=f.id AND ef.empresa_id=? WHERE f.chave=?")){ps.setLong(1,emp);ps.setString(2,chave);try(ResultSet rs=ps.executeQuery()){return rs.next()&&rs.getBoolean(1);}}} private Map evolutionAprovada(long emp)throws SQLException{try(Connection m=masterDs.getMasterConnection();PreparedStatement ps=m.prepareStatement("SELECT * FROM evolution_instancias WHERE empresa_id=? AND status='aprovada' ORDER BY aprovado_em DESC,id DESC LIMIT 1")){ps.setLong(1,emp);try(ResultSet rs=ps.executeQuery()){return rs.next()?resultMap(rs):null;}}} - private Map smtpConfig()throws SQLException{try(Connection m=masterDs.getMasterConnection();PreparedStatement ps=m.prepareStatement("SELECT chave,valor FROM configuracoes_plataforma WHERE chave LIKE 'smtp_%'")){Map out=new HashMap<>();try(ResultSet rs=ps.executeQuery()){while(rs.next())out.put(rs.getString(1),rs.getString(2));}return out;}} + public Map carregarConfigEmail()throws SQLException{return smtpConfig();} + private Map smtpConfig()throws SQLException{try(Connection m=masterDs.getMasterConnection();PreparedStatement ps=m.prepareStatement("SELECT chave,valor FROM configuracoes_plataforma WHERE chave LIKE 'smtp_%' OR chave LIKE 'imap_%'")){Map out=new HashMap<>();try(ResultSet rs=ps.executeQuery()){while(rs.next())out.put(rs.getString(1),rs.getString(2));}return out;}} private Map funcionario(long emp,long id)throws SQLException{try(Connection m=masterDs.getMasterConnection();PreparedStatement ps=m.prepareStatement("SELECT id,nome,email,telefone FROM funcionarios WHERE id=? AND empresa_id=?")){ps.setLong(1,id);ps.setLong(2,emp);try(ResultSet rs=ps.executeQuery()){return rs.next()?resultMap(rs):null;}}} private String telefoneWhatsapp(Object raw){String d=EvolutionApiClient.onlyDigits(str(raw));if((d.length()==10||d.length()==11)&&!d.startsWith("55"))return "55"+d;return d;} private String limite(String v){if(v==null)return null;return v.length()>900?v.substring(0,900):v;} diff --git a/src/main/webapp/css/app.css b/src/main/webapp/css/app.css index 79ab161..fe2317b 100644 --- a/src/main/webapp/css/app.css +++ b/src/main/webapp/css/app.css @@ -1607,3 +1607,164 @@ th { height: 28px; stroke-width: 2.5; } + +/* Configuração de E-mail (IMAP / SMTP) */ +.email-config-panel { + padding: 36px 40px; + background: #ffffff; + border: 1px solid var(--line, #d8e1ea); + border-radius: var(--radius-lg, 14px); + box-shadow: var(--shadow-sm); + margin-bottom: 24px; +} + +.email-config-header { + text-align: center; + margin-bottom: 32px; +} + +.email-config-title { + font-size: 32px; + font-weight: 300; + color: #374151; + margin: 0 0 8px 0; + letter-spacing: -0.5px; +} + +.email-config-subtitle { + font-size: 15px; + color: #6b7280; + margin: 0; + font-weight: 400; +} + +.email-config-columns { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 48px; + position: relative; +} + +@media (min-width: 769px) { + .email-config-columns::before { + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 50%; + width: 1px; + background: #e5e7eb; + transform: translateX(-50%); + } +} + +@media (max-width: 768px) { + .email-config-panel { + padding: 24px 20px; + } + .email-config-columns { + grid-template-columns: 1fr; + gap: 32px; + } +} + +.email-column { + display: flex; + flex-direction: column; + gap: 16px; +} + +.email-section-heading { + font-size: 16px; + font-weight: 600; + color: #111827; + margin: 0 0 6px 0; +} + +.email-field { + display: flex; + flex-direction: column; + gap: 6px; +} + +.email-field > label { + font-size: 13.5px; + color: #4b5563; + font-weight: 500; +} + +.email-field input, +.email-field select { + width: 100%; + padding: 10px 14px; + border: 1px solid #d1d5db; + border-radius: 6px; + font-size: 14px; + color: #111827; + background: #ffffff; + transition: border-color .15s, box-shadow .15s; +} + +.email-field input:focus, +.email-field select:focus { + border-color: var(--brand, #0f766e); + box-shadow: 0 0 0 3px rgba(15, 118, 110, 0.15); + outline: none; +} + +.email-security-wrap { + display: flex; + align-items: center; + gap: 16px; + flex-wrap: wrap; +} + +.email-security-wrap select { + width: auto; + min-width: 130px; + flex: 0 0 auto; +} + +.email-checkbox-label { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 13.5px; + color: #4b5563; + cursor: pointer; + user-select: none; + margin: 0; +} + +.email-checkbox-label input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; + accent-color: var(--brand, #0f766e); + margin: 0; +} + +.email-from-wrap { + margin-top: 24px; + padding-top: 20px; + border-top: 1px solid #f3f4f6; +} + +.email-actions-footer { + display: flex; + gap: 12px; + align-items: center; + flex-wrap: wrap; + margin-top: 24px; + padding-top: 20px; + border-top: 1px solid #e5e7eb; +} + +.email-status-feedback-box { + margin-top: 16px; + padding: 14px 18px; + border-radius: var(--radius, 8px); + background: #f9fafb; + border: 1px solid #e5e7eb; + font-size: 14px; +} diff --git a/src/main/webapp/master/configuracoes.html b/src/main/webapp/master/configuracoes.html index 11d7c1b..1563f5e 100644 --- a/src/main/webapp/master/configuracoes.html +++ b/src/main/webapp/master/configuracoes.html @@ -16,23 +16,120 @@

Configurações

-
-

E-mail

-
-
-
-
-
-
-
- +
diff --git a/src/main/webapp/master/configuracoes.js b/src/main/webapp/master/configuracoes.js index ff443bb..e84cfa6 100644 --- a/src/main/webapp/master/configuracoes.js +++ b/src/main/webapp/master/configuracoes.js @@ -24,10 +24,20 @@ const Icons = { check: '' }; -const emailKeys = ['smtp_from', 'smtp_host', 'smtp_password', 'smtp_port', 'smtp_starttls', 'smtp_user']; +function esc(v){return String(v??'').replace(/[&<>'"]/g,c=>({'&':'&','<':'<','>':'>',"'":''','"':'"'}[c]))} + +function getEmailFormData() { + const data = Util.formData(emailForm); + data.imap_allow_insecure_ssl = emailForm.elements['imap_allow_insecure_ssl']?.checked ? 'true' : 'false'; + data.smtp_allow_insecure_ssl = emailForm.elements['smtp_allow_insecure_ssl']?.checked ? 'true' : 'false'; + return data; +} + +const emailKeys = ['imap_host', 'imap_port', 'imap_security', 'imap_user', 'smtp_host', 'smtp_port', 'smtp_security', 'smtp_user', 'smtp_from']; const abacateKeys = ['abacatepay_api_key', 'abacatepay_webhook_secret']; function renderConfig(target, cfg, keys, labels) { + if (!target) return; target.innerHTML = keys.map(k => `

${labels[k] || k}: ${cfg[k] || ''}

`).join(''); } @@ -91,17 +101,16 @@ async function load() { const d = await Api.get('/api/master/configuracoes'); const cfg = d.configuracoes || {}; for (const [k, v] of Object.entries(cfg)) { - if (emailForm.elements[k]) emailForm.elements[k].value = v; - if (abacateForm.elements[k]) abacateForm.elements[k].value = v; + if (emailForm?.elements[k]) { + if (emailForm.elements[k].type === 'checkbox') { + emailForm.elements[k].checked = (v === 'true' || v === true); + } else { + emailForm.elements[k].value = v; + } + } + if (abacateForm?.elements[k]) abacateForm.elements[k].value = v; } - renderConfig(emailRows, cfg, emailKeys, { - smtp_from: 'Remetente', - smtp_host: 'Host', - smtp_password: 'Senha', - smtp_port: 'Porta', - smtp_starttls: 'StartTLS', - smtp_user: 'Usuário' - }); + if (emailRows) renderConfig(emailRows, cfg, emailKeys, {}); renderConfig(abacateRows, cfg, abacateKeys, { abacatepay_api_key: 'API Key', abacatepay_webhook_secret: 'Webhook Secret' @@ -300,15 +309,127 @@ async function rejeitar(emp, id) { emailForm.onsubmit = async e => { e.preventDefault(); + const btn = document.querySelector('#btn-salvar-email'); + if (btn) { + btn.disabled = true; + btn.textContent = 'Salvando...'; + } try { - await Api.put('/api/master/configuracoes', Util.formData(emailForm)); - Util.toast('Configurações de e-mail salvas'); - load(); + const payload = getEmailFormData(); + await Api.put('/api/master/configuracoes', payload); + Util.toast('Configurações de e-mail salvas com sucesso!'); + await load(); + testarEConectarEmail(); } catch (err) { Util.toast(err.message, true); + } finally { + if (btn) { + btn.disabled = false; + btn.textContent = 'Salvar configurações'; + } } }; +window.testarEConectarEmail = async () => { + const btn = document.querySelector('#btn-testar-email'); + const feedback = document.querySelector('#email-status-feedback'); + const headerBadges = document.querySelector('#email-header-status-badges'); + if (btn) { + btn.disabled = true; + btn.textContent = 'Testando conexão...'; + } + if (feedback) { + feedback.classList.remove('hidden'); + feedback.innerHTML = 'Testando conexões SMTP e IMAP...'; + } + try { + const payload = getEmailFormData(); + const res = await Api.post('/api/master/configuracoes/testar-email', payload); + const smtpOk = !!res?.smtp?.ok; + const imapOk = !!res?.imap?.ok; + + if (headerBadges) { + headerBadges.innerHTML = ` + SMTP: ${smtpOk ? 'Conectado' : 'Erro'} + IMAP: ${imapOk ? 'Conectado' : 'Erro'} + `; + } + + if (feedback) { + feedback.classList.remove('hidden'); + feedback.innerHTML = ` +
+ Status da Conexão: + SMTP ${smtpOk ? 'Conectado' : 'Falhou'} + IMAP ${imapOk ? 'Conectado' : 'Falhou'} +
+
+

Email de saída (SMTP): ${esc(res?.smtp?.mensagem || '')}

+

Email recebido (IMAP): ${esc(res?.imap?.mensagem || '')}

+
+ `; + } + + if (res?.ok) { + Util.toast('Conexão de e-mail (SMTP e IMAP) estabelecida com sucesso!'); + } else { + Util.toast('Falha ao conectar em um ou mais servidores de e-mail.', true); + } + } catch (err) { + if (feedback) { + feedback.classList.remove('hidden'); + feedback.innerHTML = `Erro ao testar conexão: ${esc(err.message)}`; + } + Util.toast(err.message, true); + } finally { + if (btn) { + btn.disabled = false; + btn.textContent = 'Testar e Conectar'; + } + } +}; + +window.sincronizarEmailsAgora = async () => { + const btn = document.querySelector('#btn-sync-email'); + const feedback = document.querySelector('#email-status-feedback'); + if (btn) { + btn.disabled = true; + btn.textContent = 'Sincronizando...'; + } + try { + const res = await Api.post('/api/master/configuracoes/sincronizar-email', {}); + Util.toast(res?.mensagem || 'E-mails sincronizados com sucesso.'); + if (feedback) { + feedback.classList.remove('hidden'); + feedback.innerHTML = `

Sincronização de Respostas: ${esc(res?.mensagem || '')}

`; + } + } catch (err) { + Util.toast(err.message, true); + } finally { + if (btn) { + btn.disabled = false; + btn.textContent = 'Sincronizar Respostas Agora'; + } + } +}; + +document.querySelector('#imap-port-select')?.addEventListener('change', e => { + const val = e.target.value; + const sec = document.querySelector('#imap-sec-select'); + if (!sec) return; + if (val === '993') sec.value = 'ssl_tls'; + else if (val === '143') sec.value = 'starttls'; +}); + +document.querySelector('#smtp-port-select')?.addEventListener('change', e => { + const val = e.target.value; + const sec = document.querySelector('#smtp-sec-select'); + if (!sec) return; + if (val === '465') sec.value = 'ssl_tls'; + else if (val === '587') sec.value = 'starttls'; + else if (val === '25') sec.value = 'none'; +}); + abacateForm.onsubmit = async e => { e.preventDefault(); try {