commit 278d2e078e26cab65856c1e59a9fd3d80830be73
parent 1d596be5caaac14a7e652ecba1e544150b17a8f6
Author: Sivaldo <sivaldodavi@disroot.org>
Date: Fri, 3 Apr 2026 19:54:51 -0300
Limpeza: sem grupo, sem status; adição: anexos e mídias
Diffstat:
| M | go.mod | | | 1 | + |
| M | go.sum | | | 2 | ++ |
| M | main.go | | | 205 | +++++++++++++++++++++++++++++++++---------------------------------------------- |
3 files changed, 88 insertions(+), 120 deletions(-)
diff --git a/go.mod b/go.mod
@@ -4,6 +4,7 @@ go 1.25.5
require (
github.com/BurntSushi/toml v1.6.0
+ github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
github.com/mattn/go-sqlite3 v1.14.40
github.com/mdp/qrterminal/v3 v3.2.1
go.mau.fi/whatsmeow v0.0.0-20260327181659-02ec817e7cf4
diff --git a/go.sum b/go.sum
@@ -22,6 +22,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
+github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
diff --git a/main.go b/main.go
@@ -15,6 +15,7 @@ import (
"time"
"github.com/BurntSushi/toml"
+ "github.com/jordan-wright/email"
"github.com/mdp/qrterminal/v3"
_ "github.com/mattn/go-sqlite3"
"go.mau.fi/whatsmeow"
@@ -52,52 +53,37 @@ func loadConfig() Config {
return cfg
}
-// --- Email ---
+// --- Email com suporte a Anexo ---
-func portugueseWeekday(d time.Weekday) string {
- return [...]string{"Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sab"}[d]
-}
+func sendEmailWithAttachment(cfg Config, subject, body, fileName string, fileData []byte) {
+ e := email.NewEmail()
+ e.From = fmt.Sprintf("Zap <%s>", cfg.SMTP.User)
+ e.To = []string{cfg.SMTP.User}
+ e.Subject = subject
+ e.Text = []byte(body)
+
+ if len(fileData) > 0 {
+ e.Attach(strings.NewReader(string(fileData)), fileName, "application/octet-stream")
+ }
-func sendEmailRaw(cfg Config, subject, body string) {
addr := fmt.Sprintf("%s:%d", cfg.SMTP.Host, cfg.SMTP.Port)
auth := smtp.PlainAuth("", cfg.SMTP.User, cfg.SMTP.Password, cfg.SMTP.Host)
- msg := fmt.Sprintf(
- "From: %s\r\nTo: %s\r\nSubject: %s\r\nContent-Type: text/plain; charset=UTF-8\r\n\r\n%s",
- cfg.SMTP.User, cfg.SMTP.User, subject, body,
- )
-
var err error
if cfg.SMTP.Port == 465 {
- conn, e := tls.Dial("tcp", addr, &tls.Config{ServerName: cfg.SMTP.Host})
- if e != nil {
- fmt.Fprintln(os.Stderr, "erro tls:", e)
- return
- }
- c, e := smtp.NewClient(conn, cfg.SMTP.Host)
- if e != nil {
- fmt.Fprintln(os.Stderr, "erro smtp:", e)
- return
- }
- defer c.Close()
- c.Auth(auth)
- c.Mail(cfg.SMTP.User)
- c.Rcpt(cfg.SMTP.User)
- w, _ := c.Data()
- fmt.Fprint(w, msg)
- err = w.Close()
+ err = e.SendWithTLS(addr, auth, &tls.Config{ServerName: cfg.SMTP.Host})
} else {
- err = smtp.SendMail(addr, auth, cfg.SMTP.User, []string{cfg.SMTP.User}, []byte(msg))
+ err = e.Send(addr, auth)
}
if err != nil {
fmt.Fprintln(os.Stderr, "erro ao enviar email:", err)
} else {
- fmt.Printf("[%s] email enviado: %s\n", time.Now().Format("15:04:05"), subject)
+ fmt.Printf("[%s] Email enviado: %s\n", time.Now().Format("15:04:05"), subject)
}
}
-// --- WhatsApp ---
+// --- WhatsApp Setup ---
func newClient() *whatsmeow.Client {
exePath, _ := os.Executable()
@@ -105,10 +91,7 @@ func newClient() *whatsmeow.Client {
dbPath := filepath.Join(filepath.Dir(realPath), "whatsapp.db")
dbURI := fmt.Sprintf("file:%s?_foreign_keys=on", dbPath)
- container, err := sqlstore.New(
- context.Background(), "sqlite3", dbURI,
- waLog.Stdout("Database", "ERROR", true),
- )
+ container, err := sqlstore.New(context.Background(), "sqlite3", dbURI, waLog.Stdout("Database", "ERROR", true))
if err != nil {
fmt.Fprintln(os.Stderr, "erro db:", err)
os.Exit(1)
@@ -131,11 +114,10 @@ func connectClient(client *whatsmeow.Client) {
}
}
-// --- Abook ---
+// --- Abook & Helpers ---
type Contact struct {
- Name string
- Mobile string
+ Name, Mobile string
}
func loadAbook() []Contact {
@@ -160,24 +142,18 @@ func loadAbook() []Contact {
}
if k, v, ok := strings.Cut(line, "="); ok {
switch strings.TrimSpace(k) {
- case "name":
- cur.Name = strings.TrimSpace(v)
- case "mobile":
- cur.Mobile = strings.TrimSpace(v)
+ case "name": cur.Name = strings.TrimSpace(v)
+ case "mobile": cur.Mobile = strings.TrimSpace(v)
}
}
}
- if cur.Name != "" && cur.Mobile != "" {
- contacts = append(contacts, cur)
- }
+ if cur.Name != "" && cur.Mobile != "" { contacts = append(contacts, cur) }
return contacts
}
func cleanNumber(s string) string {
return strings.Map(func(r rune) rune {
- if r >= '0' && r <= '9' {
- return r
- }
+ if r >= '0' && r <= '9' { return r }
return -1
}, s)
}
@@ -188,18 +164,17 @@ func findName(jidUser string, contacts []Contact, pushName string) string {
suffix := clean[len(clean)-8:]
for _, c := range contacts {
s := cleanNumber(c.Mobile)
- if len(s) >= 8 && s[len(s)-8:] == suffix {
- return c.Name
- }
+ if len(s) >= 8 && s[len(s)-8:] == suffix { return c.Name }
}
}
- if pushName != "" {
- return pushName
- }
- return jidUser
+ return (func() string { if pushName != "" { return pushName }; return jidUser })()
+}
+
+func portugueseWeekday(d time.Weekday) string {
+ return [...]string{"Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sab"}[d]
}
-// --- Comandos ---
+// --- Daemon ---
func runDaemon() {
cfg := loadConfig()
@@ -208,40 +183,58 @@ func runDaemon() {
client.AddEventHandler(func(evt interface{}) {
v, ok := evt.(*events.Message)
- if !ok || v.Info.IsFromMe {
- return
+ if !ok || v.Info.IsFromMe { return }
+
+ // --- FILTROS DE SEGURANÇA ---
+ // 1. Bloqueia Mensagens de Grupos (@g.us)
+ if v.Info.Chat.Server == "g.us" { return }
+
+ // 2. Bloqueia Mensagens de Status/Stories (status@broadcast)
+ if v.Info.Chat.User == "status" { return }
+ // ----------------------------
+
+ rawMsg := v.Message
+ if rawMsg.GetDeviceSentMessage() != nil {
+ rawMsg = rawMsg.GetDeviceSentMessage().GetMessage()
}
- // Tenta pegar o texto (conversa simples ou estendida)
- txt := ""
- if v.Message.Conversation != nil {
- txt = *v.Message.Conversation
- } else if v.Message.ExtendedTextMessage != nil {
- txt = *v.Message.ExtendedTextMessage.Text
+ var txt, msgType, fileName string
+ var fileData []byte
+ var err error
+ ctx := context.Background()
+
+ if m := rawMsg.GetConversation(); m != "" {
+ txt, msgType = m, "Texto"
+ } else if m := rawMsg.GetExtendedTextMessage(); m != nil {
+ txt, msgType = m.GetText(), "Texto"
+ } else if m := rawMsg.GetImageMessage(); m != nil {
+ msgType, txt, fileName = "Imagem", "Legenda: "+m.GetCaption(), "imagem.jpg"
+ fileData, err = client.Download(ctx, m)
+ } else if m := rawMsg.GetStickerMessage(); m != nil {
+ msgType, txt, fileName = "Figurinha", "[Figurinha]", "sticker.webp"
+ fileData, err = client.Download(ctx, m)
+ } else if m := rawMsg.GetAudioMessage(); m != nil {
+ msgType, txt, fileName = "Áudio", fmt.Sprintf("Duração: %ds", m.GetSeconds()), "audio.ogg"
+ fileData, err = client.Download(ctx, m)
+ } else if m := rawMsg.GetVideoMessage(); m != nil {
+ msgType, txt, fileName = "Vídeo", "Legenda: "+m.GetCaption(), "video.mp4"
+ fileData, err = client.Download(ctx, m)
}
- if txt == "" {
- return
- }
+ if msgType == "" { return }
+ if err != nil { fmt.Fprintln(os.Stderr, "Erro download:", err) }
name := findName(v.Info.Sender.User, contacts, v.Info.PushName)
- fmt.Printf("[%s] %s -> Enviando email...\n", time.Now().Format("15:04:05"), name)
-
- // Formata corpo do email
- body := fmt.Sprintf("De: %s\nData: [%s %s]\n\nMensagem:\n%s",
- name,
- portugueseWeekday(v.Info.Timestamp.Weekday()),
- v.Info.Timestamp.Format("02/01 15:04"),
- txt,
- )
-
- subject := fmt.Sprintf("WhatsApp de %s", name)
- sendEmailRaw(cfg, subject, body)
+ body := fmt.Sprintf("De: %s\nData: %s %s\nTipo: %s\n\n%s",
+ name, portugueseWeekday(v.Info.Timestamp.Weekday()),
+ v.Info.Timestamp.Format("02/01 15:04"), msgType, txt)
+
+ sendEmailWithAttachment(cfg, "Zap de "+name+" ["+msgType+"]", body, fileName, fileData)
})
connectClient(client)
- fmt.Println("daemon rodando (modo imediato). Ctrl+C para parar.")
-
+ fmt.Println("Daemon rodando (Privado apenas + Figurinha). Ctrl+C para parar.")
+
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
@@ -251,62 +244,34 @@ func runDaemon() {
func runSend() {
contacts := loadAbook()
var lines []string
- for _, c := range contacts {
- lines = append(lines, fmt.Sprintf("%s\t%s", c.Name, c.Mobile))
- }
+ for _, c := range contacts { lines = append(lines, fmt.Sprintf("%s\t%s", c.Name, c.Mobile)) }
cmd := exec.Command("fzf", "--prompt=para: ", "--delimiter=\t", "--with-nth=1")
cmd.Stdin = strings.NewReader(strings.Join(lines, "\n"))
- cmd.Stderr = os.Stderr
out, err := cmd.Output()
- if err != nil {
- return
- }
+ if err != nil { return }
+
parts := strings.SplitN(strings.TrimSpace(string(out)), "\t", 2)
- if len(parts) < 2 {
- return
- }
- chosen := Contact{Name: parts[0], Mobile: parts[1]}
- phone := cleanNumber(chosen.Mobile)
-
- jid, err := types.ParseJID(phone + "@s.whatsapp.net")
- if err != nil {
- fmt.Fprintln(os.Stderr, "numero invalido:", phone)
- os.Exit(1)
- }
+ jid, _ := types.ParseJID(cleanNumber(parts[1]) + "@s.whatsapp.net")
client := newClient()
connectClient(client)
defer client.Disconnect()
- fmt.Printf("para: %s (%s)\n", chosen.Name, chosen.Mobile)
+ fmt.Printf("Para: %s\n", parts[0])
scanner := bufio.NewScanner(os.Stdin)
for {
fmt.Print("> ")
- if !scanner.Scan() {
- break
- }
- text := strings.TrimSpace(scanner.Text())
- if text == "" {
- break
- }
- client.SendMessage(context.Background(), jid, &waE2E.Message{
- Conversation: proto.String(text),
- })
+ if !scanner.Scan() { break }
+ t := strings.TrimSpace(scanner.Text())
+ if t == "" { break }
+ client.SendMessage(context.Background(), jid, &waE2E.Message{Conversation: proto.String(t)})
}
}
func main() {
- if len(os.Args) < 2 {
- fmt.Println("uso: zap <daemon|send>")
- os.Exit(1)
- }
+ if len(os.Args) < 2 { fmt.Println("uso: zap <daemon|send>"); return }
switch os.Args[1] {
- case "daemon":
- runDaemon()
- case "send":
- runSend()
- default:
- fmt.Println("uso: zap <daemon|send>")
- os.Exit(1)
+ case "daemon": runDaemon()
+ case "send": runSend()
}
}