Parcheando MailScanner para soportar long_queue_ids y hash_queues de Postfix

Desde la versión 2.9 de Postfix, hay una directiva llamada enable_long_queue_ids, la cual viene deshabilitada por defecto, que da la posibilidad de que los IDs de los mensajes que gestiona Postfix sean más largos que los de por defecto (por ejemplo: 2FEE85E0213 Vs 3zWlWK2Vxgzd8Wj).

Sin dicha opción, cuando un mismo servidor gestiona cientos de miles de mensajes, puede darse el caso de que el ID que se le asigna a un mensaje se repita a lo largo del día (true history). Esto provoca que la gestión de logs pueda dar problemas debido a que un identificador que debería ser único, no lo es y tenemos asociado a él dos FROMs, dos TOs, dos IP origen…etc.

Además, hay otras directivas llamadas hash_queue_depth y hash_queue_names, en las que se pueden definir qué colas van a estar hasheadas, es decir, los mensajes en ellas se guardarán en un árbol de directorios (esto permite los accesos más rápidos a los mensajes, en vez de tener miles en un solo nivel). Por ejemplo:

/var/spool/postfix/hold/A/F/AF12D45
/var/spool/postfix/hold/A/D/AD64212
/var/spool/postfix/hold/3/B/3B123DF

Con los IDs de cola cortos, como en el ejemplo, se van cogiendo secuencialmente caracteres del ID de mensaje (hasta el valor de hash_queue_depth que en el ejemplo sería “2”) para ir creando subdirectorios. Con los IDs largos, esto es más complicado, como veremos más adelante.

MailScanner no tenía soporte para IDs de cola largos y las colas hasheadas, por lo que hubo que remangarse (pizza y café, como decimos por la oficina) y ponerse manos a la obra.

Recordemos cuáles son los pasos que ejecuta MailScanner para analizar los correos:

– Los mensajes que se reciben por SMTP se quedan retenidos en la cola hold de Postfix (por el header_checks correspondiente), en el directorio /var/spool/postfix/hold
– MailScanner, en procesos batch, va analizando dicha cola recogiendo los mensajes allí retenidos
– Una vez analizados contra los servicios antispam, antivirus…los deja en la cola incoming de Postfix (/var/spool/postfix/incoming/)
– Postfix los trata y los enviará a la cola de entrega correspondiente (smtp, lmtp, pipe…)

Por lo tanto, debemos de modificar MailScanner para soportar ese tipo de IDs al cogerlos de hold para tratarlos (si no, al tener un formato más largo no es capaz de “verlos”) y posteriormente, para dejarlos en incoming.

Para saber cómo crea Postfix los IDs de cola largos, revisamos su código fuente y nos encontramos en el archivo src/global/mail_queue.h lo siguiente:


# The long non-repeating queue ID is encoded in an alphabet of 10 digits,
# 21 upper-case characters, and 21 or fewer lower-case characters. The
# alphabet is made "safe" by removing all the vowels (AEIOUaeiou). The ID
# is the concatenation of:
#
# - the time in seconds (base 52 encoded, six or more chars),
#
# - the time in microseconds (base 52 encoded, exactly four chars),
#
# - the 'z' character to separate the time and inode information,
#
# - the inode number (base 51 encoded so that it contains no 'z').

es decir, la concatenación del tiempo en ese momento en segundos (en base52), el tiempo en microsegundos (en base52 también), el carácter “z” y el número de inodo del archivo que contiene el mensaje temporal (en base51, para evitar repetir esa “z”). En los IDs cortos, es más sencillo, basta con concatenar el tiempo en microsegundos con el número de inodo del archivo temporal. Recordemos la diferencia: 2FEE85E0213 Vs 3zWlWK2Vxgzd8Wj.

Para leer el mensaje de la cola hold, si dicha cola está “hasheada”, debemos saber cuál es la ruta completa del mensaje para poder acceder a él. Esto, con los ID de cola largos, es más complejo, como decíamos antes (no son los caracteres secuenciales del ID, como con IDs cortos). El formato en el que Postfix crea estas colas hasheadas es (por ejemplo con depth 2):

/var/spool/postfix/hold/4/0/3zWns51s8lzBxp8
/var/spool/postfix/hold/0/D/3zWnsP0NWwzBxnY
/var/spool/postfix/hold/0/E/3zWnsP0PbyzBxnd

cuyo proceso de generación es:

– coger los 4 caracteres que representan los microsegundos que están en base52
– ordenarlos de forma inversa
– decodificarlo de base52
– convertir a hexadecimal

A partir de dicho resultado, ahora sí, se irán cogiendo los caracteres uno a uno y accediendo a subdirectorios para tener la ruta completa del mensaje.
Esto se realiza en el archivo PFDiskStore.pm:

  #
  # Alvaro Marin alvaro@hostalia.com - 2016/08/25
  #
  # Long queue IDs and hash queue support
  #
  my $long_queue_id=0;
  my $hex=$this->{hdname};
  if ($this->{hdname} !~ /^[A-F0-9]+$/) {
   # long queue id
   $long_queue_id=1;
   # With long queue IDs, when hash queues is enabled, the directory hierarchy
   # is not generated with first characters of the ID (as with short queue IDs):
   # we have to get the 4 characters that represent the microseconds and then:
   # reverse them + decode from base 52 + convert to hexadecimal
   my $msecs=reverse substr $this->{hdname},6,4;
   my $total=0;
   my $count=0;
   my %BASE52_CHARACTERS = (0 => "0",1 => "1",2 => "2",3 => "3",4 => "4",5 => "5",
6 => "6",7 => "7",8 => "8",9 => "9", 10 => "B",11 => "C",12 => "D",13 => "F",
14 => "G",15 => "H",16 => "J",17 => "K",18 => "L", 19 => "M",20 => "N",21 => "P",
22 => "Q",23 => "R",24 => "S",25 => "T",26 => "V",27 => "W", 28 => "X",29 => "Y",
30 => "Z",31 => "b",32 => "c",33 => "d",34 => "f",35 => "g",36 => "h", 37 => "j",
38 => "k",39 => "l",40 => "m",41 => "n",42 => "p",43 => "q",44 => "r",45 => "s",
46 => "t",47 => "v",48 => "w",49 => "x",50 => "y",51 => "z");
    # To avoid using external modules...reverse the hash
    my %rBASE52_CHARACTERS = reverse %BASE52_CHARACTERS;
    for my $c (split //, $msecs) {
        my $index = $rBASE52_CHARACTERS{$c};
              $total+=$index * (52**$count);
              $count++;
    }
    $hex = sprintf("%05X", $total); # 5 chars...from Postfix's code!
    #print STDERR "Microseconds of ".$this->{hdname}.":$msecs -> HEX: $hex\n";
  }
  if ($MailScanner::SMDiskStore::HashDirDepth == 2) {
    if ($long_queue_id){
            $hex =~ /^(.)(.)(.*)$/;
            $this->{hdpath} = "$dir/$1/$2/" . $this->{hdname};
    } else {
            $this->{hdname} =~ /^(.)(.)(.*)$/;
            $this->{hdpath} = "$dir/$1/$2/" . $this->{hdname};
   }
  }
  elsif ($MailScanner::SMDiskStore::HashDirDepth == 1) {
    if ($long_queue_id){
      $hex =~ /^(.)(.*)$/;
            $this->{hdpath} = "$dir/$1/" . $this->{hdname};
    } else {
            $this->{hdname} =~ /^(.)(.*)$/;
            $this->{hdpath} = "$dir/$1/" . $this->{hdname};
    }
  }
  elsif ($MailScanner::SMDiskStore::HashDirDepth == 0) {
    $this->{hdname} =~ /^(.*)$/;
    $this->{hdpath} = "$dir/" . $this->{hdname};
  }

Como se ve en el código, se realizan dichos pasos y se genera la ruta completa del mensaje, dependiendo de la profundidad definida en la variable hash_queue_depth, usando los primeros caracteres del resultado de las operaciones que comentábamos antes, que queda guardado en la variable $hex del código.

MailScanner por tanto, ya podrá acceder al mensaje para analizarlo. Realizará las operaciones oportunas y estará ya preparado para dejarlo en la cola incoming para que así, Postfix lo recoja y lo entregue en el destino. Este proceso debe hacerse también con un ID de cola largo por lo que debemos generarlo (de la forma ya comentada anteriormente). Esto lo haremos en Postfix.pm:

    #
    # Alvaro Marin alvaro@hostalia.com - 2016/08/25
    # 
    # Support for Postfix's long queue IDs format (enable_long_queue_ids).
    # The name of the file created in the outgoing queue will be the queue ID. 
    # We'll generate it like Postfix does. From src/global/mail_queue.h :
    #
    # The long non-repeating queue ID is encoded in an alphabet of 10 digits,
    # 21 upper-case characters, and 21 or fewer lower-case characters. The
    # alphabet is made "safe" by removing all the vowels (AEIOUaeiou). The ID
    # is the concatenation of:
    #
    # - the time in seconds (base 52 encoded, six or more chars),
    # 
    # - the time in microseconds (base 52 encoded, exactly four chars),
    # 
    # - the 'z' character to separate the time and inode information,
    #
    # - the inode number (base 51 encoded so that it contains no 'z').
    #
    #
    # We don't know if Postfix has long queue IDs enabled so we must check it 
    # using the temporaly filename:
    # Short queue IDs: /var/spool/postfix/incoming/temp-14793-6773D15E4E9.A3F46
    # Long queue IDs: /var/spool/postfix/incoming/temp-17735-3sK9pc0mftzJX5P.A38B9
    #
    my $long_queue_id=0;
    my $hex;
    if ($file =~ /\-[A-Za-z0-9]{12,20}\.[A-Za-z0-9]{5}$/) {
        my $file_orig=$file;
        # Long queue IDs
        $long_queue_id=1;
        my $seconds=0;
        my $microseconds=0;
        use Time::HiRes qw( gettimeofday );
        ($seconds, $microseconds) = gettimeofday;
        my $microseconds_orig=$microseconds;
        my @BASE52_CHARACTERS = ("0","1","2","3","4","5","6","7","8","9",
                                "B","C","D","F","G","H","J","K","L","M",
                                "N","P","Q","R","S","T","V","W","X","Y",
                                "Z","b","c","d","f","g","h","j","k","l",
                                "m","n","p","q","r","s","t","v","w","x","y","z");
        my $encoded='';
        my $file_out;
        my $count=0;
        while ($count < 6) {
                $encoded.=$BASE52_CHARACTERS[$seconds%52];
                $seconds/=52;
                $count++;
        }
        $file_out=reverse $encoded;
        $encoded='';
        $count=0;
        while ($count < 4) {
                $encoded.=$BASE52_CHARACTERS[$microseconds%52];
                $microseconds/=52;
                $count++;
        }
        $file_out.=reverse $encoded;

        $file_out.="z";
        my $inode=(stat("$file"))[1];
        $encoded='';
        $count=0;
        while ($count < 4) {
                $encoded.=$BASE52_CHARACTERS[$inode%51];
                $inode/=51;
                $count++;
        }
        $file=$file_out.reverse $encoded;
        # We need this for later use...
        $hex = sprintf("%05X", $microseconds_orig);
        #print STDERR "long_queue_id: New Filename is $file\n";

        # We check the generated ID...
        if ($file !~ /[A-Za-z0-9]{12,20}/) {
                # Something has gone wrong, back to short ID for safety
                MailScanner::Log::WarnLog("ERROR generating long queue ID ($file), back to short ID ($file_orig)");
                $file = sprintf("%05X%lX", int(rand 1000000)+1, (stat($file_orig))[1]);
                $long_queue_id=0;
        }
    }
    else {
        # Short queue IDs
        # Bad hash key $file = sprintf("%05X%lX", time % 1000000, (stat($file))[1]);
        # Add 1 so the number is never zero (defensive programming)
        $file = sprintf("%05X%lX", int(rand 1000000)+1, (stat($file))[1]);
        #print STDERR "New Filename is $file\n";
    }
    if ($MailScanner::SMDiskStore::HashDirDepth == 2) {
        if ($long_queue_id){
                # hash queues with long queue IDs
                $hex =~ /^(.)(.)/;
                return ($dir,$1,$2,$file);
        }
        else {
                # hash queues with short queue IDs
                $file =~ /^(.)(.)/;
                return ($dir,$1,$2,$file);
        }
    } elsif ($MailScanner::SMDiskStore::HashDirDepth == 1) {
        if ($long_queue_id){
                # hash queues with long queue IDs
                $hex =~ /^(.)/;
                return ($dir,$1,$file);
        }
        else {
                # hash queues with short queue IDs
                $file =~ /^(.)/;
                return ($dir,$1,$file);
        }
    } elsif ($MailScanner::SMDiskStore::HashDirDepth == 0) {
      return ($dir,$file);
    } else {
      MailScanner::Log::WarnLog("Postfix dir depth has not been set!");
    }

el proceso de generación del ID de cola (que será el mismo que el nombre de archivo a generar donde guardar el mensaje en la cola incoming) es el que comentábamos antes, la concatenación del tiempo en segundos (en base52), el tiempo en microsegundos (en base52 también), el carácter “z” y el número de inodo (en base51). Luego, según la profundidad de la configuración de hash de colas, generaremos los subdirectorios correspondientes.

Y con todo esto, ya tenemos MailScanner con soporte para estas dos funcionalidades. Está ya parcheado con este código desde hace varias versiones y funcionando perfectamente. Todo ello gracias a Perl y al software libre :-)

Deja un comentario

Tu dirección de correo electrónico no será publicada.