================================================================== Un sistema automatico di incartellamento della posta elettronica ================================================================== ovvero, come io gestisco la mia posta elettronica ================================================= :Author: dakkar@thenautilus.net :Date: 2009-01-10 Il mio archivio di posta elettronica conta, al momento, 74711 messaggi. Sono iscritto a circa 40 mailing list (per la maggior parte di esse non tengo copia di tutti i messaggi). Come faccio a raccapezzarmici? Innanzi tutto, ogni mailing list ha una sua cartella, e |procmail|_ è sufficiente per gestire questa parte; ad esempio, per mandare nella cartella apposita i messaggi della lista `mongers`:: # mongers :0: * (^From|^TO|List-Id:).*mongers[@.](lists\.)?perl\.it .mail.Perl.mongers/ Come potreste notare, se aveste perso un eccesso di tempo a studiare |procmail|, la mia posta è archiviata in formato |maildir|_. Altre regole |procmail| mettono in cartelle apposite i messaggi provenienti da mittenti particolari (gente che conosco, negozi, etc). Accedo alla posta tramite |dovecot|_ (server) e |claws|_ (client). *Non* uso una cartella "messaggi inviati": |claws| è configurato in modo da salvare i messaggi che scrivo nella cartella che "sto guardando" [#savehere]_. In pratica, il risultato è che le mie risposte stanno nella stessa cartella dei messaggi a cui rispondono. In questo modo ho sempre i thread *interamente* visibili. .. [#savehere] Tasto destro sulla radice delle cartelle (nella lista cartelle), "Properties…", sezione "Compose", marcare "Save copy of outgoing messages to this folder instead of Sent", marcare anche il chekcbox a destra ("Apply to subfolders"), pulsante "Apply". A questo punto, se le mie cartelle fossero tutte e sole quelle in cui |procmail| mette i messaggi, sarei a posto. "Peccato" che mi servano anche cartelle di altro genere: ad esempio, per argomento. I messaggi finiscono in queste cartelle solo perché ce li sposto io, manualmente, dal client. Per completare l'automatizzazione del tutto, manca un componente: un sistema per cui un messaggio di risposta che arriva, finisca nella stessa cartella in cui sta il messaggio a cui risponde, anche se quest'ultimo è stato spostato a mano. Incartellamento delle risposte ------------------------------ Come dice la |rfc2822|_, se un messaggio è una risposta, dovrà avere un campo (nell'header) ``In-Reply-To:`` il cui valore sia il "message id" del messaggio a cui sta rispondendo. Inoltre, il campo ``References:`` contiene i "message id" di tutta una sequenza di messaggi del thread [#thread]_. .. [#thread] Sì, lo so che è un po' più complicato di così, ma questa è un'approssimazione sufficiente per i nostri fini. Chi vuole i dettagli può leggersi la |rfc2822|_ e |jwz|_. Di conseguenza, per sapere in quale cartella mettere un messaggio, basta trovare in quale cartella stia il messaggio il cui campo ``Message-ID:`` abbia lo stesso valore di uno dei "message id" riferiti nel messaggio in arrivo. Ovviamente, se il messaggio in arrivo riferisce più di un messaggio, potremmo avere una situazione ambigua; per fortuna, per come sono usati di solito i campi dei riferimenti, l'ambiguità si risolve esaminando prima il valore in ``In-Reply-To:``, poi quelli in ``References:`` dall'ultimo al primo (al solito, i dettagli sono nella |rfc2822|). Che esista un solo messaggio con un dato "message id" è vero a meno che io non abbia due copie di uno stesso messaggio; possiamo ignorare il problema. Un'implementazione banale consisterebbe in poco più che un ``grep`` ricorsivo. Funziona, ma è un po' lenta: rischia di impiegare svariati secondi, e andrebbe eseguita per ciascun messaggio che arriva. Un metodo più furbo sta nel tenersi un indice che mappi i "message id" alle cartelle. Per tenerlo aggiornato, però, dobbiamo tenere conto che nuovi messaggi vengono aggiunti (o perché arrivano, o perché sono copie dei messaggi che io invio) e quelli esistenti spostati. Ci serve un modo per intercettare questi cambiamenti, e aggiornare l'indice. Il problema di notare modifiche in una cartella è tanto comune (serve, ad esempio, ai file manager) che Linux fornisce una soluzione a livello di kernel: |inotify|_. Siccome usarlo non è esattamente banale, mi sono appoggiato a |inotify-tools|_. A questo punto ho tutti i pezzi che servono per scrivere il server che gestisca l'indice dei messaggi: - all'inizio, il server si scandisce l'archivio di posta e si segna in quale cartella sta ciascun "message id" - nel frattempo, usa ``inotifywait`` (parte di |inotify-tools|) per tenere traccia delle modifiche - il server ascolta su una socket, e quando gli viene inviato l'header di un messaggio, ne estrae i campi dei riferimenti, e risponde con il nome della cartella in cui deve andare questo messaggio (o nulla, se il messaggio non è una risposta, o non abbiamo il messaggio cui risponde) Avendo questo, si scrive un semplice client:: #!/bin/bash netcat localhost 9000 | tr -d '\015\012' e due regole |procmail|:: :0 hi AUTOFOLDER=| ~/bin/find_folder :0 * AUTOFOLDER ?? ^\.mail\. $AUTOFOLDER/ e i messaggi finiscono automaticamente dove devono. .. warning:: |procmail| e ``Out of memory`` Alcune versioni di |procmail| (di sicuro la 3.22-r7 distribuita in Gentoo) hanno un bug che causa un'allocazione esagerata di memoria quando si tenta di catturare l'output di un programma in una variabile (come nelle regole precedenti). Se vi capita, controllate se la vostra distribuzione ha una versione corretta; altrimenti, potete sempre applicare la patch_. Come è fatto il server ---------------------- Dalla descrizione del server si può notare come esso debba occuparsi di svariate funzioni contemporaneamente: scandire l'archivio, ricevere le notifiche di cambiamenti, rispondere alle richieste del client. In Perl ci sono sostanzialmente due strategie per affrontare il problema: * gestione asincrona degli eventi (ad esempio con |poe|_) * multi-threading A me i thread, pure con i piccoli problemi che hanno in Perl, risultano più "naturali". Ho perciò strutturato il server in questo modo: - un thread riceve le notifiche da ``inotifywait``, e aggiorna l'indice - un thread scandisce l'archivio, e aggiorna l'indice (questo thread termina una volta scandito tutto l'indice) - il thread principale si sospende sulla socket - quando arriva una richiesta sulla socket, un nuovo thread viene creato per servirla Il programma_ non è molto complesso. Il grosso del lavoro è fatto da |email-simple|_ (per estrarre i campi dai messaggi) e |file-next|_ (per scandire l'archivio). Un paio di note sulle strutture dati: per gestire correttamente aggiunte, rimozioni e spostamenti, e non confondersi con le manovre che fa |dovecot| [#manovre]_, uso due hash: ``%files2id`` mappa dalla coppia mailbox-nomefile al "message id" (ovvero, ``$files2id{$mailbox}->{$file}=$id``); ``%id2mailbox`` mappa da un "message id" a una *lista* di mailbox. Sì, una lista: questo sia per non avere errori in caso di messaggi duplicati, sia per le menzionate manovre di |dovecot|. Il server restituisce sempre l'ultima mailbox nella lista, che è tenuta in ordine cronologico. Inoltre, siccome queste strutture sono lette e scritte da thread diversi, è necessario assicurare la mutua esclusione degli accessi tramite lock. .. [#manovre] Ad esempio, per spostare un messaggio, prima lo duplica nella cartella di destinazione, poi lo cancella da quella di origine. Altro materiale --------------- * il programma_, colorato da |ppi| * il sorgente_ nudo e crudo * il pod_ .. |procmail| replace:: ``procmail`` .. _procmail: http://www.procmail.org/ .. |maildir| replace:: ``MailDir++`` .. _maildir: http://www.inter7.com/courierimap/README.maildirquota.html .. |dovecot| replace:: ``dovecot`` .. _dovecot: http://www.dovecot.org/ .. |claws| replace:: `Claws Mail` .. _claws: http://www.claws-mail.org/ .. |rfc2822| replace:: `RFC` 2822 .. _rfc2822: http://www.faqs.org/rfcs/rfc2822.html .. |jwz| replace:: l'algoritmo di threading di Jamie Zawinski .. _jwz: http://www.jwz.org/doc/threading.html .. |inotify| replace:: ``inotify`` .. _inotify: http://en.wikipedia.org/wiki/Inotify .. |inotify-tools| replace:: ``inotify-tools`` .. _`inotify-tools`: http://inotify-tools.sourceforge.net/ .. |poe| replace:: ``POE`` .. _poe: http://search.cpan.org/dist/POE/ .. |email-simple| replace:: ``Email::Simple`` .. _`email-simple`: http://search.cpan.org/dist/Email-Simple/ .. |file-next| replace:: ``File::Next`` .. _`file-next`: http://search.cpan.org/dist/File-Next/ .. |ppi| replace:: ``PPI::HTML`` .. _ppi: http://search.cpan.org/dist/PPI-HTML/ .. _programma: maildir-indexer.pl.html .. _sorgente: maildir-indexer.pl .. _pod: maildir-indexer.pod.html .. _patch: http://bugs.gentoo.org/200006