Como reportar spam a spamcop desde GMail


spam

Introducción

Este post es un poco diferente a los demás, ya que es una pequeña “herramienta” que hice para facilitar el reporte de spam a SpamCop.net. Esto ayuda a reducir el verdadero spam de fuentes desconocidas. Por alguna razón, empecé a recibir como 40 correos al día (todos iban a la carpeta de spam), pero igual resultaba molesto. Así que comencé a reportarlos a SpamCop, pero el proceso era algo lento y me cansé rápido, así que creé este “script” para hacer todo más fácil. Básicamente, lo que hace es listar todos los mensajes en la carpeta de spam, los descarga y luego los reenvía como adjuntos a SpamCop. Después de eso, recibís un mail con un enlace para confirmar el envío, ¡y listo!

Hay algunos pre-requisitos, como habilitar la API de GMail para tu cuenta. Podés hacerlo aquí. Después de eso, la primera vez que uses la app, tendrás que autorizarla. Esto lo hacés pegando la URL que la app te da en el navegador, luego haces clic en “Permitir”, y después copiás el token que te da de vuelta en la terminal (solo se hace una vez). Después de eso, solo corrés el binario en un cronjob o tal vez como una lambda (aunque todavía no llegué a eso). Normalmente, reviso la carpeta de spam, elimino lo que no creo que sea spam o lo que sea, y luego corro el script para reportar todo lo que claramente es spam. Toma unos segundos, y luego recibo el enlace para confirmar todos los reportes (uno por uno, lamentablemente). Este script no es perfecto, ya que a veces SpamCop no puede leer correctamente el correo reenviado, pero he revisado exportándolos como archivo y los veo bien, así que será algo para investigar otro día. Este script lo hice en unas 2-4 horas, sin tener conocimientos previos de la API de GMail ni nada.

También necesitás configurar una cuenta de SpamCop, que vas a usar para enviar tus reportes. Podés hacerlo aquí.

El código fuente lo podés encontrar aquí.


Código

He agregado algunos comentarios en el código para que sea más fácil de entender.

package main

import (
    "crypto/rand"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "log"
    "net/http"
    "os"

    "golang.org/x/net/context"
    "golang.org/x/oauth2"
    "golang.org/x/oauth2/google"
    "google.golang.org/api/gmail/v1"
)

// Recupera un token, lo guarda y devuelve el cliente generado.
func getClient(config *oauth2.Config) *http.Client {
    // El archivo token.json almacena los tokens de acceso y actualización del usuario,
    // y se crea automáticamente cuando el flujo de autorización se completa por primera vez.
    tokFile := "token.json"
    tok, err := tokenFromFile(tokFile)
    if err != nil {
        tok = getTokenFromWeb(config)
        saveToken(tokFile, tok)
    }
    return config.Client(context.Background(), tok)
}

// Solicita un token desde la web, y luego devuelve el token recuperado.
func getTokenFromWeb(config *oauth2.Config) *oauth2.Token {
    authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
    fmt.Printf("Ve al siguiente enlace en tu navegador y luego ingresá el código de autorización: 
%v
", authURL)

    var authCode string
    if _, err := fmt.Scan(&authCode); err != nil {
        log.Fatalf("No se pudo leer el código de autorización: %v", err)
    }

    tok, err := config.Exchange(context.TODO(), authCode)
    if err != nil {
        log.Fatalf("No se pudo recuperar el token desde la web: %v", err)
    }
    return tok
}

// Recupera un token desde un archivo local.
func tokenFromFile(file string) (*oauth2.Token, error) {
    f, err := os.Open(file)
    if err != nil {
        return nil, err
    }
    defer f.Close()
    tok := &oauth2.Token{}
    err = json.NewDecoder(f).Decode(tok)
    return tok, err
}

// Guarda un token en una ruta de archivo.
func saveToken(path string, token *oauth2.Token) {
    fmt.Printf("Guardando credenciales en el archivo: %s
", path)
    f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
    if err != nil {
        log.Fatalf("No se pudo guardar el token de oauth: %v", err)
    }
    defer f.Close()
    json.NewEncoder(f).Encode(token)
}

func randStr(strSize int, randType string) string {
    var dictionary string

    if randType == "alphanum" {
        dictionary = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
    }

    if randType == "alpha" {
        dictionary = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
    }

    if randType == "number" {
        dictionary = "0123456789"
    }

    var bytes = make([]byte, strSize)
    rand.Read(bytes)
    for k, v := range bytes {
        bytes[k] = dictionary[v%byte(len(dictionary))]
    }
    return string(bytes)
}

func main() {
    b, err := ioutil.ReadFile("credentials.json")
    if err != nil {
        log.Fatalf("No se pudo leer el archivo de credenciales del cliente: %v", err)
    }

    // Si modificás estos alcances, eliminá tu token.json guardado previamente.
    // Tené en cuenta que este alcance le dará a la app acceso completo a tu cuenta, así que tené cuidado (MailGoogleComScope).
    // Si no querés eliminar correos, pero solo leer y reportar (enviar),
    // entonces podés usar (gmail.GmailReadonlyScope, gmail.GmailComposeScope) en lugar de (MailGoogleComScope).
    // En ese caso, comentá las líneas desde la 178 hasta la 181.
    config, err := google.ConfigFromJSON(b, gmail.MailGoogleComScope)
    if err != nil {
        log.Fatalf("No se pudo analizar el archivo de credenciales del cliente: %v", err)
    }
    client := getClient(config)

    srv, err := gmail.New(client)
    if err != nil {
        log.Fatalf("No se pudo recuperar el cliente de Gmail: %v", err)
    }

    pageToken := ""
    for {
        req := srv.Users.Messages.List("me").Q("in:spam")
        if pageToken != "" {
            req.PageToken(pageToken)
        }
        r, err := req.Do()
        if err != nil {
            log.Fatalf("No se pudo recuperar los mensajes: %v", err)
        }

        log.Printf("Procesando %v mensajes...
", len(r.Messages))
        for _, m := range r.Messages {
            // Necesitamos usar Raw para poder obtener todo de una vez.
            msg, err := srv.Users.Messages.Get("me", m.Id).Format("raw").Do()
            if err != nil {
                log.Fatalf("No se pudo recuperar el mensaje %v: %v", m.Id, err)
            }

            // Nuevo mensaje para nuestro servicio de Gmail para enviar
            var message gmail.Message
            boundary := randStr(32, "alphanum")
            // Se debe decodificar de la codificación URL, de lo contrario pueden ocurrir cosas extrañas.
            body, err := base64.URLEncoding.DecodeString(msg.Raw)

            messageBody := []byte("Content-Type: multipart/mixed; boundary=" + boundary + " 
" +
                "MIME-Version: 1.0
" +
                "To: " + "[email protected]" + "
" +
                "From: " + "[email protected]" + "
" +
                "Subject: " + "Spam report" + "

" +

                "--" + boundary + "
" +
                "Content-Type: text/plain; charset=" + string('"') + "UTF-8" + string('"') + "
" +
                "MIME-Version: 1.0
" +
                "Content-Transfer-Encoding: 7bit

" +
                "Spam report" + "

" +
                "--" + boundary + "
" +

                "Content-Type: " + "message/rfc822" + "; name=" + string('"') + "email.txt" + string('"') + " 
" +
                "MIME-Version: 1.0
" +
                "Content-Transfer-Encoding: base64
" +
                "Content-Disposition: attachment; filename=" + string('"') + "email.txt" + string('"') + " 

" +
                string(body) +
                "--" + boundary + "--")

            // ver https://godoc.org/google.golang.org/api/gmail/v1#Message en .Raw
            // ¡Usá URLEncoding aquí! StdEncoding será rechazado por la API de Google

            message.Raw = base64.URLEncoding.EncodeToString(messageBody)

            // Envía el mensaje
            _, err = srv

.Users.Messages.Send("me", &message).Do()

            if err != nil {
                log.Printf("Error: %v", err)
            } else {
                fmt.Println("¡Mensaje enviado!")

                // Si todo salió bien hasta acá, entonces elimina el mensaje.
                if err := srv.Users.Messages.Delete("me", m.Id).Do(); err != nil {
                    log.Fatalf("no se pudo eliminar el mensaje %v: %v", m.Id, err)
                }
                log.Printf("Mensaje eliminado %v.
", m.Id)
            }
        }

        if r.NextPageToken == "" {
            break
        }
        pageToken = r.NextPageToken
    }
}

Ejecutándolo
$ spam
2019/12/31 17:45:14 Procesando 2 mensajes...
Mensaje enviado!
Mensaje eliminado 1ac83e1f8.
Mensaje enviado!
Mensaje eliminado 2ac89cbd3.

Fuentes

Algunos artículos, páginas y archivos que usé y me ayudaron a hacer lo que quería hacer:

Notas adicionales

Aunque esto aún necesita algo de trabajo, espero que mantenga mi cuenta limpia y tal vez ayude a alguien que esté pensando en hacer lo mismo.


Errata

Si encontrás algún error o tenés alguna sugerencia, por favor enviame un mensaje para que lo pueda corregir.



No tienes cuenta? Regístrate aqui

Ya registrado? Iniciar sesión a tu cuenta ahora.

Iniciar session con GitHub
Iniciar sesion con Google
  • Comentarios

    Online: 0

Por favor inicie sesión para poder escribir comentarios.

by Gabriel Garrido