feat(master): reformular configuracao de email com IMAP/SMTP, teste de conexao e sincronizacao de respostas

This commit is contained in:
Tiago
2026-10-13 17:05:39 -03:00
parent 65dc2c92be
commit d547c2d4bb
7 changed files with 814 additions and 51 deletions
@@ -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<String, String> 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;
@@ -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<String, String> 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<String, Object> testarSmtp(Map<String, String> 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<String, Object> testarImap(Map<String, String> 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<String, String> 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<Long> 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] : ("<email-" + msg.getMessageNumber() + "-" + (msg.getSentDate() != null ? msg.getSentDate().getTime() : System.currentTimeMillis()) + ">");
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<String, Object> 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<String, String> 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<String, String> 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;
}
}
@@ -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<String,Object> 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<String,String> b = body(ex);
Set<String> permitidas = Set.of("smtp_host","smtp_port","smtp_user","smtp_password","smtp_from","smtp_starttls","abacatepay_api_key","abacatepay_webhook_secret");
Set<String> 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<String, String> b = body(ex);
Map<String, String> merged = carregarConfigEmailCompleta();
for (Map.Entry<String, String> entry : b.entrySet()) {
String val = entry.getValue();
if (val != null && !val.isBlank() && !"********".equals(val)) {
merged.put(entry.getKey(), val);
}
}
Map<String, Object> smtpRes = emailClient.testarSmtp(merged);
Map<String, Object> 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<String, String> 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<String, String> 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<String, String> 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<String, String> body = body(ex);
String nome = required(body, "nome");
@@ -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<String,Object> 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<String,String> smtpConfig()throws SQLException{try(Connection m=masterDs.getMasterConnection();PreparedStatement ps=m.prepareStatement("SELECT chave,valor FROM configuracoes_plataforma WHERE chave LIKE 'smtp_%'")){Map<String,String> out=new HashMap<>();try(ResultSet rs=ps.executeQuery()){while(rs.next())out.put(rs.getString(1),rs.getString(2));}return out;}}
public Map<String,String> carregarConfigEmail()throws SQLException{return smtpConfig();}
private Map<String,String> 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<String,String> out=new HashMap<>();try(ResultSet rs=ps.executeQuery()){while(rs.next())out.put(rs.getString(1),rs.getString(2));}return out;}}
private Map<String,Object> 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;}
+161
View File
@@ -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;
}
+112 -15
View File
@@ -16,23 +16,120 @@
<h1>Configurações</h1>
</div>
<section class="panel">
<h2>E-mail</h2>
<form id="email-form" class="grid cols">
<div class="field"><label>Host</label><input name="smtp_host"></div>
<div class="field"><label>Porta</label><input name="smtp_port" type="number"></div>
<div class="field"><label>Usuário</label><input name="smtp_user"></div>
<div class="field"><label>Senha</label><input name="smtp_password" type="password"></div>
<div class="field"><label>Remetente</label><input name="smtp_from"></div>
<div class="field"><label>StartTLS</label>
<select name="smtp_starttls">
<option value="true">Sim</option>
<option value="false">Não</option>
</select>
<section class="email-config-panel">
<div class="email-config-header">
<h2 class="email-config-title">Configurar Conta</h2>
<p class="email-config-subtitle">Preencha as configurações de IMAP e SMTP abaixo para conectar sua conta.</p>
</div>
<form id="email-form">
<div class="email-config-columns">
<!-- Coluna 1: Email Recebido (IMAP) -->
<div class="email-column">
<h3 class="email-section-heading">Email Recebido (IMAP):</h3>
<div class="email-field">
<label>Server:</label>
<input name="imap_host" placeholder="imap.seconectai.com.br">
</div>
<div class="email-field">
<label>Porta:</label>
<select name="imap_port" id="imap-port-select">
<option value="993">993</option>
<option value="143">143</option>
</select>
</div>
<div class="email-field">
<label>Segurança:</label>
<div class="email-security-wrap">
<select name="imap_security" id="imap-sec-select">
<option value="ssl_tls">SSL / TLS</option>
<option value="starttls">STARTTLS</option>
<option value="none">Nenhum</option>
</select>
<label class="email-checkbox-label">
<input type="checkbox" name="imap_allow_insecure_ssl" value="true">
Permitir SSL inseguro
</label>
</div>
</div>
<div class="email-field">
<label>Username:</label>
<input name="imap_user" placeholder="no_reply@seconectai.com.br">
</div>
<div class="email-field">
<label>Senha:</label>
<input name="imap_password" type="password" placeholder="••••••••••••">
</div>
</div>
<!-- Coluna 2: Email de saída (SMTP) -->
<div class="email-column">
<h3 class="email-section-heading">Email de saída (SMTP):</h3>
<div class="email-field">
<label>Server:</label>
<input name="smtp_host" placeholder="smtp.seconectai.com.br">
</div>
<div class="email-field">
<label>Porta:</label>
<select name="smtp_port" id="smtp-port-select">
<option value="465">465</option>
<option value="587">587</option>
<option value="25">25</option>
</select>
</div>
<div class="email-field">
<label>Segurança:</label>
<div class="email-security-wrap">
<select name="smtp_security" id="smtp-sec-select">
<option value="ssl_tls">SSL / TLS</option>
<option value="starttls">STARTTLS</option>
<option value="none">Nenhum</option>
</select>
<label class="email-checkbox-label">
<input type="checkbox" name="smtp_allow_insecure_ssl" value="true">
Permitir SSL inseguro
</label>
</div>
</div>
<div class="email-field">
<label>Username:</label>
<input name="smtp_user" placeholder="no_reply@seconectai.com.br">
</div>
<div class="email-field">
<label>Senha:</label>
<input name="smtp_password" type="password" placeholder="••••••••••••">
</div>
</div>
</div>
<button class="btn">Salvar e-mail</button>
<!-- Remetente opcional -->
<div class="email-from-wrap">
<div class="email-field">
<label>Remetente de envio (From opcional):</label>
<input name="smtp_from" placeholder="Ex: no_reply@seconectai.com.br ou Notificações &lt;no_reply@seconectai.com.br&gt;">
<small style="color: var(--muted); font-size: 12px; margin-top: 4px;">Se deixado em branco, o Username SMTP será utilizado como remetente.</small>
</div>
</div>
<div class="email-actions-footer">
<button class="btn" type="submit" id="btn-salvar-email">Salvar configurações</button>
<button class="btn secondary" type="button" onclick="testarEConectarEmail()" id="btn-testar-email">Testar e Conectar</button>
<button class="btn secondary" type="button" onclick="sincronizarEmailsAgora()" id="btn-sync-email">Sincronizar Respostas Agora</button>
<span id="email-header-status-badges" style="margin-left: auto; display: flex; gap: 8px;"></span>
</div>
<div id="email-status-feedback" class="email-status-feedback-box hidden"></div>
</form>
<div id="email-rows"></div>
</section>
<section class="panel">
+135 -14
View File
@@ -24,10 +24,20 @@ const Icons = {
check: '<svg viewBox="0 0 24 24" aria-hidden="true"><polyline points="20 6 9 17 4 12"></polyline></svg>'
};
const emailKeys = ['smtp_from', 'smtp_host', 'smtp_password', 'smtp_port', 'smtp_starttls', 'smtp_user'];
function esc(v){return String(v??'').replace(/[&<>'"]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;',"'":'&#39;','"':'&quot;'}[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 => `<p><strong>${labels[k] || k}</strong>: ${cfg[k] || ''}</p>`).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 = '<span class="muted">Testando conexões SMTP e IMAP...</span>';
}
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 = `
<span class="badge ${smtpOk ? 'ok' : 'warn'}">SMTP: ${smtpOk ? 'Conectado' : 'Erro'}</span>
<span class="badge ${imapOk ? 'ok' : 'warn'}">IMAP: ${imapOk ? 'Conectado' : 'Erro'}</span>
`;
}
if (feedback) {
feedback.classList.remove('hidden');
feedback.innerHTML = `
<div style="display: flex; gap: 12px; align-items: center; margin-bottom: 8px;">
<strong>Status da Conexão:</strong>
<span class="badge ${smtpOk ? 'ok' : 'warn'}">SMTP ${smtpOk ? 'Conectado' : 'Falhou'}</span>
<span class="badge ${imapOk ? 'ok' : 'warn'}">IMAP ${imapOk ? 'Conectado' : 'Falhou'}</span>
</div>
<div style="font-size: 13.5px; line-height: 1.6;">
<p style="margin: 4px 0;"><strong>Email de saída (SMTP):</strong> ${esc(res?.smtp?.mensagem || '')}</p>
<p style="margin: 4px 0;"><strong>Email recebido (IMAP):</strong> ${esc(res?.imap?.mensagem || '')}</p>
</div>
`;
}
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 = `<span style="color:var(--danger)">Erro ao testar conexão: ${esc(err.message)}</span>`;
}
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 = `<p style="margin: 0; color: var(--brand-strong);"><strong>Sincronização de Respostas:</strong> ${esc(res?.mensagem || '')}</p>`;
}
} 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 {