{"id":338,"date":"2018-02-05T10:00:26","date_gmt":"2018-02-05T09:00:26","guid":{"rendered":"http:\/\/www.alvaromarin.com\/?p=338"},"modified":"2018-02-05T10:47:44","modified_gmt":"2018-02-05T09:47:44","slug":"parcheando-mailscanner-soportar-long_queue_ids-hash_queues-postfix","status":"publish","type":"post","link":"http:\/\/www.alvaromarin.com\/2018\/02\/05\/parcheando-mailscanner-soportar-long_queue_ids-hash_queues-postfix\/","title":{"rendered":"Parcheando MailScanner para soportar long_queue_ids y hash_queues de Postfix"},"content":{"rendered":"
Desde la versi\u00f3n 2.9 de Postfix, hay una directiva llamada enable_long_queue_ids<\/em>, la cual viene deshabilitada por defecto, que da la posibilidad de que los IDs de los mensajes que gestiona Postfix sean m\u00e1s largos que los de por defecto (por ejemplo: 2FEE85E0213 Vs 3zWlWK2Vxgzd8Wj).<\/p>\n Sin dicha opci\u00f3n, 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\u00eda (true history). Esto provoca que la gesti\u00f3n de logs pueda dar problemas debido a que un identificador que deber\u00eda ser \u00fanico, no lo es y tenemos asociado a \u00e9l dos FROMs, dos TOs, dos IP origen…etc.<\/p>\n Adem\u00e1s, hay otras directivas llamadas hash_queue_depth<\/em> y hash_queue_names<\/em>, en las que se pueden definir qu\u00e9 colas van a estar hasheadas<\/em>, es decir, los mensajes en ellas se guardar\u00e1n en un \u00e1rbol de directorios (esto permite los accesos m\u00e1s r\u00e1pidos a los mensajes, en vez de tener miles en un solo nivel). Por ejemplo:<\/p>\n 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<\/em> que en el ejemplo ser\u00eda \u00ab2\u00bb) para ir creando subdirectorios. Con los IDs largos, esto es m\u00e1s complicado, como veremos m\u00e1s adelante.<\/p>\n MailScanner no ten\u00eda soporte para IDs de cola largos y las colas hasheadas, por lo que hubo que remangarse (pizza y caf\u00e9, como decimos por la oficina) y ponerse manos a la obra.<\/p>\n <\/p>\n Recordemos cu\u00e1les son los pasos que ejecuta MailScanner para analizar los correos:<\/p>\n – Los mensajes que se reciben por SMTP se quedan retenidos en la cola hold<\/em> de Postfix (por el header_checks<\/em> correspondiente), en el directorio \/var\/spool\/postfix\/hold<\/em> Por lo tanto, debemos de modificar MailScanner para soportar ese tipo de IDs al cogerlos de hold<\/em> para tratarlos (si no, al tener un formato m\u00e1s largo no es capaz de \u00abverlos\u00bb) y posteriormente, para dejarlos en incoming<\/em>.<\/p>\n Para saber c\u00f3mo crea Postfix los IDs de cola largos, revisamos su c\u00f3digo fuente y nos encontramos en el archivo src\/global\/mail_queue.h<\/em> lo siguiente:<\/p>\n es decir, la concatenaci\u00f3n del tiempo en ese momento en segundos (en base52), el tiempo en microsegundos (en base52 tambi\u00e9n), el car\u00e1cter \u00abz\u00bb y el n\u00famero de inodo del archivo que contiene el mensaje temporal (en base51, para evitar repetir esa \u00abz\u00bb). En los IDs cortos, es m\u00e1s sencillo, basta con concatenar el tiempo en microsegundos con el n\u00famero de inodo del archivo temporal. Recordemos la diferencia: 2FEE85E0213 Vs 3zWlWK2Vxgzd8Wj.<\/p>\n Para leer el mensaje de la cola hold<\/em>, si dicha cola est\u00e1 \u00abhasheada\u00bb, debemos saber cu\u00e1l es la ruta completa del mensaje para poder acceder a \u00e9l. Esto, con los ID de cola largos, es m\u00e1s complejo, como dec\u00edamos 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):<\/p>\n cuyo proceso de generaci\u00f3n es:<\/p>\n – coger los 4 caracteres que representan los microsegundos que est\u00e1n en base52 A partir de dicho resultado, ahora s\u00ed, se ir\u00e1n cogiendo los caracteres uno a uno y accediendo a subdirectorios para tener la ruta completa del mensaje. Como se ve en el c\u00f3digo, se realizan dichos pasos y se genera la ruta completa del mensaje, dependiendo de la profundidad definida en la variable hash_queue_depth<\/em>, usando los primeros caracteres del resultado de las operaciones que coment\u00e1bamos antes, que queda guardado en la variable $hex<\/em> del c\u00f3digo.<\/p>\n MailScanner por tanto, ya podr\u00e1 acceder al mensaje para analizarlo. Realizar\u00e1 las operaciones oportunas y estar\u00e1 ya preparado para dejarlo en la cola incoming<\/em> para que as\u00ed, Postfix lo recoja y lo entregue en el destino. Este proceso debe hacerse tambi\u00e9n con un ID de cola largo por lo que debemos generarlo (de la forma ya comentada anteriormente). Esto lo haremos en Postfix.pm:<\/p>\n el proceso de generaci\u00f3n del ID de cola (que ser\u00e1 el mismo que el nombre de archivo a generar donde guardar el mensaje en la cola incoming<\/em>) es el que coment\u00e1bamos antes, la concatenaci\u00f3n del tiempo en segundos (en base52), el tiempo en microsegundos (en base52 tambi\u00e9n), el car\u00e1cter \u00abz\u00bb y el n\u00famero de inodo (en base51). Luego, seg\u00fan la profundidad de la configuraci\u00f3n de hash de colas, generaremos los subdirectorios correspondientes.<\/p>\n Y con todo esto, ya tenemos MailScanner con soporte para estas dos funcionalidades. Est\u00e1 ya parcheado con este c\u00f3digo desde hace varias versiones y funcionando perfectamente. Todo ello gracias a Perl y al software libre :-)<\/p>\n","protected":false},"excerpt":{"rendered":" Desde la versi\u00f3n 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\u00e1s largos que los de por defecto (por … Sigue leyendo \/var\/spool\/postfix\/hold\/A\/F\/AF12D45
\n\/var\/spool\/postfix\/hold\/A\/D\/AD64212
\n\/var\/spool\/postfix\/hold\/3\/B\/3B123DF<\/code><\/p>\n
\n – MailScanner, en procesos batch<\/em>, va analizando dicha cola recogiendo los mensajes all\u00ed retenidos
\n – Una vez analizados contra los servicios antispam, antivirus…los deja en la cola incoming<\/em> de Postfix (\/var\/spool\/postfix\/incoming\/<\/em>)
\n – Postfix los trata y los enviar\u00e1 a la cola de entrega correspondiente (smtp, lmtp, pipe…)<\/p>\n
\n # The long non-repeating queue ID is encoded in an alphabet of 10 digits,
\n # 21 upper-case characters, and 21 or fewer lower-case characters. The
\n # alphabet is made \"safe\" by removing all the vowels (AEIOUaeiou). The ID
\n # is the concatenation of:
\n #
\n # - the time in seconds (base 52 encoded, six or more chars),
\n #
\n # - the time in microseconds (base 52 encoded, exactly four chars),
\n #
\n # - the 'z' character to separate the time and inode information,
\n #
\n # - the inode number (base 51 encoded so that it contains no 'z').
\n<\/code><\/p>\n\/var\/spool\/postfix\/hold\/4\/0\/3zWns51s8lzBxp8
\n\/var\/spool\/postfix\/hold\/0\/D\/3zWnsP0NWwzBxnY
\n\/var\/spool\/postfix\/hold\/0\/E\/3zWnsP0PbyzBxnd<\/code><\/p>\n
\n – ordenarlos de forma inversa
\n – decodificarlo de base52
\n – convertir a hexadecimal<\/p>\n
\nEsto se realiza en el archivo PFDiskStore.pm:<\/p>\n\r\n #\r\n # Alvaro Marin alvaro@hostalia.com - 2016\/08\/25\r\n #\r\n # Long queue IDs and hash queue support\r\n #\r\n my $long_queue_id=0;\r\n my $hex=$this->{hdname};\r\n if ($this->{hdname} !~ \/^[A-F0-9]+$\/) {\r\n # long queue id\r\n $long_queue_id=1;\r\n # With long queue IDs, when hash queues is enabled, the directory hierarchy\r\n # is not generated with first characters of the ID (as with short queue IDs):\r\n # we have to get the 4 characters that represent the microseconds and then:\r\n # reverse them + decode from base 52 + convert to hexadecimal\r\n my $msecs=reverse substr $this->{hdname},6,4;\r\n my $total=0;\r\n my $count=0;\r\n my %BASE52_CHARACTERS = (0 => "0",1 => "1",2 => "2",3 => "3",4 => "4",5 => "5",\r\n6 => "6",7 => "7",8 => "8",9 => "9", 10 => "B",11 => "C",12 => "D",13 => "F",\r\n14 => "G",15 => "H",16 => "J",17 => "K",18 => "L", 19 => "M",20 => "N",21 => "P",\r\n22 => "Q",23 => "R",24 => "S",25 => "T",26 => "V",27 => "W", 28 => "X",29 => "Y",\r\n30 => "Z",31 => "b",32 => "c",33 => "d",34 => "f",35 => "g",36 => "h", 37 => "j",\r\n38 => "k",39 => "l",40 => "m",41 => "n",42 => "p",43 => "q",44 => "r",45 => "s",\r\n46 => "t",47 => "v",48 => "w",49 => "x",50 => "y",51 => "z");\r\n # To avoid using external modules...reverse the hash\r\n my %rBASE52_CHARACTERS = reverse %BASE52_CHARACTERS;\r\n for my $c (split \/\/, $msecs) {\r\n my $index = $rBASE52_CHARACTERS{$c};\r\n $total+=$index * (52**$count);\r\n $count++;\r\n }\r\n $hex = sprintf("%05X", $total); # 5 chars...from Postfix's code!\r\n #print STDERR "Microseconds of ".$this->{hdname}.":$msecs -> HEX: $hex\\n";\r\n }\r\n if ($MailScanner::SMDiskStore::HashDirDepth == 2) {\r\n if ($long_queue_id){\r\n $hex =~ \/^(.)(.)(.*)$\/;\r\n $this->{hdpath} = "$dir\/$1\/$2\/" . $this->{hdname};\r\n } else {\r\n $this->{hdname} =~ \/^(.)(.)(.*)$\/;\r\n $this->{hdpath} = "$dir\/$1\/$2\/" . $this->{hdname};\r\n }\r\n }\r\n elsif ($MailScanner::SMDiskStore::HashDirDepth == 1) {\r\n if ($long_queue_id){\r\n $hex =~ \/^(.)(.*)$\/;\r\n $this->{hdpath} = "$dir\/$1\/" . $this->{hdname};\r\n } else {\r\n $this->{hdname} =~ \/^(.)(.*)$\/;\r\n $this->{hdpath} = "$dir\/$1\/" . $this->{hdname};\r\n }\r\n }\r\n elsif ($MailScanner::SMDiskStore::HashDirDepth == 0) {\r\n $this->{hdname} =~ \/^(.*)$\/;\r\n $this->{hdpath} = "$dir\/" . $this->{hdname};\r\n }\r\n<\/pre>\n
\r\n #\r\n # Alvaro Marin alvaro@hostalia.com - 2016\/08\/25\r\n # \r\n # Support for Postfix's long queue IDs format (enable_long_queue_ids).\r\n # The name of the file created in the outgoing queue will be the queue ID. \r\n # We'll generate it like Postfix does. From src\/global\/mail_queue.h :\r\n #\r\n # The long non-repeating queue ID is encoded in an alphabet of 10 digits,\r\n # 21 upper-case characters, and 21 or fewer lower-case characters. The\r\n # alphabet is made "safe" by removing all the vowels (AEIOUaeiou). The ID\r\n # is the concatenation of:\r\n #\r\n # - the time in seconds (base 52 encoded, six or more chars),\r\n # \r\n # - the time in microseconds (base 52 encoded, exactly four chars),\r\n # \r\n # - the 'z' character to separate the time and inode information,\r\n #\r\n # - the inode number (base 51 encoded so that it contains no 'z').\r\n #\r\n #\r\n # We don't know if Postfix has long queue IDs enabled so we must check it \r\n # using the temporaly filename:\r\n # Short queue IDs: \/var\/spool\/postfix\/incoming\/temp-14793-6773D15E4E9.A3F46\r\n # Long queue IDs: \/var\/spool\/postfix\/incoming\/temp-17735-3sK9pc0mftzJX5P.A38B9\r\n #\r\n my $long_queue_id=0;\r\n my $hex;\r\n if ($file =~ \/\\-[A-Za-z0-9]{12,20}\\.[A-Za-z0-9]{5}$\/) {\r\n my $file_orig=$file;\r\n # Long queue IDs\r\n $long_queue_id=1;\r\n my $seconds=0;\r\n my $microseconds=0;\r\n use Time::HiRes qw( gettimeofday );\r\n ($seconds, $microseconds) = gettimeofday;\r\n my $microseconds_orig=$microseconds;\r\n my @BASE52_CHARACTERS = ("0","1","2","3","4","5","6","7","8","9",\r\n "B","C","D","F","G","H","J","K","L","M",\r\n "N","P","Q","R","S","T","V","W","X","Y",\r\n "Z","b","c","d","f","g","h","j","k","l",\r\n "m","n","p","q","r","s","t","v","w","x","y","z");\r\n my $encoded='';\r\n my $file_out;\r\n my $count=0;\r\n while ($count < 6) {\r\n $encoded.=$BASE52_CHARACTERS[$seconds%52];\r\n $seconds\/=52;\r\n $count++;\r\n }\r\n $file_out=reverse $encoded;\r\n $encoded='';\r\n $count=0;\r\n while ($count < 4) {\r\n $encoded.=$BASE52_CHARACTERS[$microseconds%52];\r\n $microseconds\/=52;\r\n $count++;\r\n }\r\n $file_out.=reverse $encoded;\r\n\r\n $file_out.="z";\r\n my $inode=(stat("$file"))[1];\r\n $encoded='';\r\n $count=0;\r\n while ($count < 4) {\r\n $encoded.=$BASE52_CHARACTERS[$inode%51];\r\n $inode\/=51;\r\n $count++;\r\n }\r\n $file=$file_out.reverse $encoded;\r\n # We need this for later use...\r\n $hex = sprintf("%05X", $microseconds_orig);\r\n #print STDERR "long_queue_id: New Filename is $file\\n";\r\n\r\n # We check the generated ID...\r\n if ($file !~ \/[A-Za-z0-9]{12,20}\/) {\r\n # Something has gone wrong, back to short ID for safety\r\n MailScanner::Log::WarnLog("ERROR generating long queue ID ($file), back to short ID ($file_orig)");\r\n $file = sprintf("%05X%lX", int(rand 1000000)+1, (stat($file_orig))[1]);\r\n $long_queue_id=0;\r\n }\r\n }\r\n else {\r\n # Short queue IDs\r\n # Bad hash key $file = sprintf("%05X%lX", time % 1000000, (stat($file))[1]);\r\n # Add 1 so the number is never zero (defensive programming)\r\n $file = sprintf("%05X%lX", int(rand 1000000)+1, (stat($file))[1]);\r\n #print STDERR "New Filename is $file\\n";\r\n }\r\n if ($MailScanner::SMDiskStore::HashDirDepth == 2) {\r\n if ($long_queue_id){\r\n # hash queues with long queue IDs\r\n $hex =~ \/^(.)(.)\/;\r\n return ($dir,$1,$2,$file);\r\n }\r\n else {\r\n # hash queues with short queue IDs\r\n $file =~ \/^(.)(.)\/;\r\n return ($dir,$1,$2,$file);\r\n }\r\n } elsif ($MailScanner::SMDiskStore::HashDirDepth == 1) {\r\n if ($long_queue_id){\r\n # hash queues with long queue IDs\r\n $hex =~ \/^(.)\/;\r\n return ($dir,$1,$file);\r\n }\r\n else {\r\n # hash queues with short queue IDs\r\n $file =~ \/^(.)\/;\r\n return ($dir,$1,$file);\r\n }\r\n } elsif ($MailScanner::SMDiskStore::HashDirDepth == 0) {\r\n return ($dir,$file);\r\n } else {\r\n MailScanner::Log::WarnLog("Postfix dir depth has not been set!");\r\n }\r\n<\/pre>\n