next up previous contents
Next: 8 Segnalare un bug Up: Manuale di FreePOPs Previous: 6 Plugin   Contents

Subsections

7 Creare un plugin

Seguono due sezioni, la prima è una panoramica veloce su cosa un plugin deve fare, la seconda è un tutorial più dettagliato. Prima di procedere oltre suggeriamo di leggere un po' di documentazione alla base della scrittura dei plugin:

  1. Dato che i plugin sono scritti in LUA dovete leggere almeno il tutorial LUA (HTTP://lua-users.org/wiki/LuaTutorial); molte grazie a chi l'ha scritto. LUA è un linguaggio di scripting piuttosto semplice, facile da imparare, e facile da leggere. Se siete interessati a questo linguaggio dovreste leggere IL libro su LUA (``Programming in LUA'' di Roberto Ierusalimschy HTTP://www.inf.puc-rio.br/~roberto/book/). è davvero un buon libro, credetemi. Oggi il libro è stato pubblicato on line http://www.lua.org/pil/
  2. Visto che dobbiamo implementare un backend POP3 dovreste sapere cos'è il POP3. La RFC 1939 è inclusa nella directory doc/ del pacchetto dei sorgenti di FreePOPs, ma potete prelevarla anche dalla rete HTTP://www.ietf.org/rfc/rfc1939.txt.
  3. Leggete attentamente questo tutorial, è lontano dall'essere ben fatto ma è meglio di niente.
  4. Il sito web contiene, nella sezione doc, un bel po' di documentazione sui sorgenti. Dovreste tenere un web browser aperto alla pagina della documentazione sui moduli LUA mentre scrivete un plugin.
  5. Dopo aver creato un prototipo, dovreste leggere un plugin completo. Il plugin libero.lua è davvero ben commentato, iniziate pure da li'.
  6. Ricordate che questo software ha un forum ufficiale (HTTP://freepops.diludovico.it) e degli autori a cui potete chiedere aiuto.
  7. FreePOPs è distribuito sotto licenza GNU/GPL. Questo significa che ogni software che fa uso del codice di FreePOPs deve essere rilasciato sotto la stessa licenza. Questo include i plugin. Per maggiori informazioni leggete il testo della licenza nel file COPYING incluso o su HTTP://www.gnu.org/licenses/gpl.html.

7.1 Panoramica sui plugin

Un plugin è essenzialmente un backend per un server POP3. I plugin sono scritti in LUA1 mentre il server POP3 è scritto in C. Qui esamineremo l' interfaccia tra il nucleo C e i plugin LUA.

7.2 L' interfaccia tra il nucleo C ed un plugin

Come abbiamo spiegato prima il frontend POP3 in C deve essere collegato ad un backend in LUA. L'interfaccia è molto semplice se conoscete il protocollo POP3. Qui riassumiamo brevemente il significato, ma la RFC 1939 (inclusa nella directory doc/ della distribuzione dei sorgenti) è molto breve e facile da leggere. Come il vostro intuito dovrebbe suggerirvi il client POP3 può richiedere che il server POP3 conosca qualcosa delle mail che sono nella mailbox e prima o poi prelevare/cancellare dei messaggi. E questo è esattamente ciò che fa.

Il backend deve implementare tutti i comandi POP3 (come USER, PASS, RETR, DELE, QUIT, LIST, ...) e deve restituire al frontend il risultato. Diamo un semplice esempio di una sessione POP3 dalla RFC:

0.5

     1  S: <wait for connection on TCP port 110>

     2  C: <open connection>

     3  S:    +OK POP3 server 

     4  C:    USER linux@kernel.org

     5  S:    +OK now insert the pasword

     6  C:    PASS gpl

     7  S:    +OK linux's maildrop has 2 messages (320 octets)

     8  C:    STAT

     9  S:    +OK 1 320

    10  C:    LIST

    11  S:    +OK 2 messages (320 octets)

    12  S:    1 320

    13  S:    .

    14  C:    RETR 1

    15  S:    +OK 120 octets

    16  S:    <the POP3 server sends message 1>

    17  S:    .

    18  C:    DELE 1

    19  S:    +OK message 1 deleted

    20  C:    QUIT

    21  S:    +OK dewey POP3 server signing off (maildrop empty)

    22  C:  <close connection>

    23  S:  <wait for next connection>

In questa sessione il backend verrà chiamato per le righe 4, 6, 8, 10, 14, 18, 20 (tutte le righe C: ) e rispettivamente le funzioni che implementano i comandi POP3 verranno chiamate in questo modo

0.5

    user(p,"linux@kernel.org")

    pass(p,"gpl")

    stat(p)

    list_all(p)

    retr(p,1)

    dele(p,1)

    quit_update(p)

più tardi chiariremo cos'è p. Speriamo di toglierlo e renderlo implicito per completa trasparenza. è facile capire che c'è un mapping 1-1 tra i comandi POP3 e le chiamate a funzione del plugin. Potete vedere un plugin come l'implementazione dell'interfaccia POP3.

7.3 L'interfaccia tra un plugin e il nucleo C

Prendiamo in esame la chiamata a pass(p,''gpl''). Qui il plugin dovrebbe autenticare l'utente (se c'è un qualche tipo di autenticazione) e informare il nucleo C del risultato. Per ottenere questo ogni funzione dei plugin deve restituire un flag di errore, per essere più precisi uno di questi errori:

Code Significato
POPSERVER_ERR_OK


Nel nostro caso i codici d'errore più appropriati sono POPSERVER_ERR_AUTH e POPSERVER_ERR_OK. Questo è un caso semplice, in cui un codice d'errore è abbastanza. Ora analizziamo il caso più complesso della chiamata a list_all(p). Qui dobbiamo restituire un codice d'errore come prima, ma dobbiamo anche informare il nucleo C della grandezza di tutti i messaggi nella mailbox. Qui abbiamo bisogno del parametro p passato ad ogni funzione del plugin (notate che tale parametro potra' divenire implicito in futuro). p indica la struttura dati che il C si aspetta venga riempita chiamando funzioni appropriate come set_mailmessage_size(p,num,size) dove num è il numero del messaggio e size è la grandezza in byte. Solitamente è molto comune mettere insieme più funzioni. Per esempio quando guardate la pagina di una webmail con la lista di messaggi conoscete il numero dei messaggi, la loro grandezza e lo UIDL così che potete riempire la struttura dati p con tutte le informazioni per LIST, STAT, UIDL.

L'ultimo caso che esaminiamo è retr(p,num,data). poiché un messaggio di posta può essere molto grande, non è un modo elegante di scaricare l'intero messaggio senza far sì che il client di posta si lamenti per la morte del server. La soluzione è usare un callback. Ogni volta che un plugin ha dei dati da mandare al client dovrebbe chiamare la popserver_callback(buffer,data). data è una struttura opaca che il popserver necessita per compiere il suo lavoro (notate che questo parametro potrà venire rimosso per semplicità). In alcuni casi, per esempio se sapete che il messaggio è piccolo o state lavorando su una rete veloce, potete prelevare l'intero messaggio e mandarlo, ma ricordate che questo consuma più memoria.

7.4 L'arte di scrivere plugin (tutorial sui plugin)

In questa sezione scriveremo un plugin passo passo, esaminando ogni dettaglio importante. Non scriveremo un vero e completo plugin poiché può diventare un pò difficile da seguire, ma creeremo una webmail ad-hoc per i nostri scopi.

7.4.1 (step 1) Lo scheletro

La prima cosa che faremo sarà copiare il file skeleton.lua in foo.lua (perché scriveremo il plugin per la webmail foo.xx , xx sta per un dominio vero, ma non vogliamo menzionare alcun sito qui...). Ora con il vostro editor migliore (suggeriamo vim su Unix e scintilla per win32, visto che supportano il syntax highlighting per LUA, ma qualsiasi altro editor di testo va bene) aprite foo.lua e cambiate le prime righe aggiungendo il nome del plugin, la versione, il vostro nome, il vostro indirizzo email e un breve commento, nei posti appropriati.0.5

-- ************************************************************************** --

--  FreePOPs @--put domain here-- webmail interface

--

--  $Id: manual-it.tex,v 1.44 2007/10/28 12:31:49 gareuselesinge Exp $

--

--  Released under the GNU/GPL license

--  Written by --put Name here-- <--put email here-->

-- ************************************************************************** --



PLUGIN_VERSION = "--put version here--"

PLUGIN_NAME = "--put name here--"

Ora abbiamo un plugin vuoto, ma non è abbastanza per iniziare a farci hacking. Dobbiamo aprire il file config.lua (nella distribuzione win32 si trova nella directory principale, mentre nella distribuzione Unix è in /etc/freepops/; altre copie di questo file possono essere incluse nelle distribuzioni, ma sono copie di backup) e aggiungete una riga come questa0.5

-- foo plugin

freepops.MODULES_MAP["foo.xx"]      = {name="foo.lua"}

all'inizio del file. Prima di finire il primo passo dovreste provare se il plugin viene correttamente attivato da FreePOPs quando necessario. Per questo dovremo aggiungere alcune righe a foo.lua, in particolare dovremo aggiungere un valore di ritorno di errore a user().0.5

-- -------------------------------------------------------------------------- --

-- Must save the mailbox name

function user(pstate,username)

        return POPSERVER_ERR_AUTH

end

Ora la funzione user fallisce sempre, restituendo un errore di autenticazione. Dovrete ora lanciare FreePOPs (se è già in esecuzione non è necessario farlo ripartire) e lanciare telnet (sotto win32 dovreste aprire un prompt DOS, sotto Unix avrete una shell) e digitate telnet localhost 2000 e poi digitate user test@foo.xx.0.5

tassi@garfield:~$ telnet localhost 2000

Trying 127.0.0.1...

Connected to garfield.

Escape character is '^]'.

+OK FreePOPs/0.0.10 pop3 server ready

user test@foo.xx

-ERR AUTH FAILED

Connection closed by foreign host.

Il server risponde chiudendo la connessione e stampando un messaggio di autorizzazione fallita (va bene, dato che la funzione user() del nostro plugin restituisce questo errore). Nel file standard error (la console sotto Unix, il file stderr.txt sotto Windows) vengono stampati i messaggi d'errore, non vi prestate attenzione per ora.

7.4.2 (step 2) Il login

La procedura di login è la prima cosa da fare. Il protocollo POP3 ha due comandi per il login, user e pass. Prima il client esegue uno user, poi dice al server la password. Come abbiamo già visto nella panoramica questo significa che prima verrà eseguito user() e poi pass(). Questo è un esempio di login:0.5

tassi@garfield:~$ telnet localhost 2000

Trying 127.0.0.1...

Connected to garfield.

Escape character is '^]'.

+OK FreePOPs/0.0.10 pop3 server ready

user test@foo.xx

+OK PLEASE ENTER PASSWORD

pass hello

-ERR AUTH FAILED

Se lanciate FreePOPs con il parametro -w dovreste leggere questo sullo standard error/standard output:0.5

freepops started with loglevel 2 on a little endian machine.

Cannot create pid file "/var/run/freepopsd.pid"

DBG(popserver.c, 162): [5118] ?? Ip address 0.0.0.0 real port 2000

DBG(popserver.c, 162): [5118] ?? Ip address 127.0.0.1 real port 2000

DBG(popserver.c, 162): [5118] -> +OK FreePOPs/0.0.10 pop3 server ready

DBG(popserver.c, 162): [5118] <- user test@foo.xx

DBG(log_lua.c,  83): (@src/lua/foo.lua, 37) : FreePOPs plugin 'Foo web mail' version '0.0.1' started!

*** the user wants to login as 'test@foo.xx'

DBG(popserver.c, 162): [5118] -> +OK PLEASE ENTER PASSWORD

DBG(popserver.c, 157): [5118] <- PASS *********

*** the user inserted 'hello' as the password for 'test@foo.xx'

DBG(popserver.c, 162): [5118] -> -ERR AUTH FAILED

AUTH FAILED

DBG(threads.c,  81): thread 0 will die

il plugin è stato modificato un pò per memorizzare i dati dell'utente e stampare delle informazioni di debug. Questo è il plugin che ha dato questo output:0.5

foo_globals= {

username="nothing",

password="nothing"

}

-- -------------------------------------------------------------------------- --

-- Must save the mailbox name

function user(pstate,username)

foo_globals.username = username

print("*** the user wants to login as '"..username.."'")

return POPSERVER_ERR_OK

end

-- -------------------------------------------------------------------------- --

-- Must login

function pass(pstate,password)

foo_globals.password = password

print("*** the user inserted '"..password..

    "' as the password for '"..foo_globals.username.."'")

return POPSERVER_ERR_AUTH end

-- -------------------------------------------------------------------------- --

-- Must quit without updating

function quit(pstate)

return POPSERVER_ERR_OK

end

Qui vediamo delle importanti novità. Per prima cosa, la tabella foo_globals che contiene tutti i valori globali (valori che devono essere a disposizione di chiamate a funzioni successive) di cui abbiamo bisogno. Per ora ci abbiamo messo il nome utente e la password. La funzioneuser() ora memorizza il nome utente passato nella tabella foo_globals e stampa qualcosa sullo standard output. La funzione pass() allo stesso modo memorizza la password nella tabella globale e stampa qualcosa. La funzione quit() restituisce semplicemente POPSERVER_ERR_OK per far felice FreePOPs.

Ora che sappiamo come FreePOPs si comporterà durante il login dobbiamo implementare il login nella webmail, ma prima decommentiamo alcune righe nella funzione init() (chiamata alla partenza del plugin), la quale carica il modulo browser.lua (il modulo usato per fare login nella webmail). Ecco la pagina di login della webmail vista con Mozilla e il codice sorgente della stessa pagina (con Mozilla lo si vede con Ctrl-U, figura 5).

Figure 5: login
\includegraphics[scale=0.8]{EPS/login.eps}

<html>

<head>

<title>foo.xx webmail login</title>

</head>

<body style="background-color : grey; color : white">

<h1>Webmail login</h1>

<form name="webmail" method="post" action="http://localhost:3000/">

login: <input type="text" size="10" name="username"> <br>

password: <input type="password" size="10" name="password"> <br>

<input type="submit" value="login">

</form>

</body>

</html>

Abbiamo due campi di input, uno chiamato username e uno chiamato password. Quando l'utente fa click su login il browser web eseguira' POST sul HTTP://localhost:3000/ contenuto del form (ho usato un indirizzo locale per comodita', ma dovrebbe essere qualcosa come HTTP://webmail.foo.xx/login.php). Questo è ciò che il browser invia:0.5

POST / HTTP/1.1

Host: localhost:3000

User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.6) Gecko/20040614 Firefox/0.8 Accept: */*

Accept-Language: en-us,en;q=0.5

Accept-Encoding: gzip,deflate

Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7

Keep-Alive: 300

Connection: keep-alive

Content-Type: application/x-www-form-urlencoded

Content-Length: 37



username=test%40foo.xx&password=hello

Non ci interessa la prima parte (l'header HTTP, visto che il modulo browser se ne occuperà), bensì l'ultima, i dati inviati. poiché i campi del form erano username e password, i dati inviati sono 
username=test%40.foo.xx&password=hello. Ora vogliamo riprodurre la stessa richiesta HTTP con il nostro plugin. Questo è il semplice codice che fara' proprio quello.0.5

-- -------------------------------------------------------------------------- --

-- Must login

function pass(pstate,password)

foo_globals.password = password



print("*** the user inserted '"..password..

     "' as the password for '"..foo_globals.username.."'")



-- create a new browser

local b = browser.new()



-- store the browser object in globals

foo_globals.browser = b



       -- create the data to post      

       local post_data = string.format("username=%s&password=%s",

               foo_globals.username,foo_globals.password)

       -- the uri to post to   

       local post_uri = "http://localhost:3000/"



       -- post it      

       local file,err = nil, nil       

  

       file,err = b:post_uri(post_uri,post_data)

       

       print("we received this webpage: ".. file)      

       return POPSERVER_ERR_AUTH 

end

Prima creiamo un oggetto browser, poi mettiamo insieme post_uri e post_data usando un semplice string.format (una funzione simile a printf). E questa è la richiesta risultante0.5

POST / HTTP/1.1 

User-Agent: Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.6) Gecko/20040322 Firefox/0.8

Pragma: no-cache 

Accept: */* 

Host: localhost 

Content-Length: 35 

Content-Type: application/x-www-form-urlencoded



username=test@foo.xx&password=hello

questo è essenzialmente come lo volevamo fare (dovremmo fare url-encode dei post data con curl.escape()). Abbiamo salvato l'oggetto browser sulla tabella globale, perché vogliamo usare lo stesso browser tutte le volte.

Ora che abbiamo fatto login, vogliamo controllare la pagina risultante, e magari estrarre un ID di sessione che useremo poi. Questo è il codice per estrarre l'ID di sessione e la pagina HTML che abbiamo ricevuto in risposta alla richiesta di login0.5

 

        ... come sopra qui ...

     

        print("we received this webpage: ".. file)

        

        -- search the session ID        

        local _,_,id = string.find(file,"session_id=(%w+)")



        if id == nil then               

               return POPSERVER_ERR_AUTH

        end



        foo_globals.session_id = id

        return POPSERVER_ERR_OK

end

e questa è la pagina web restituita (vedi figura 6).

Figure 6: logindone
\includegraphics[scale=0.8]{EPS/logindone.eps}
0.5

<html> 

<head> 

<title>foo.xx webmail</title> 

</head> 

<body style="background-color : grey; color : white"> 

<h1>Webmail - test@foo.xx</h1> 

Login done! click here to view the inbox folder. 

<a href="http://localhost:3000/inbox.php?session_id=ABCD1234">inbox</a> 

</body> 

</html>

Notate che abbiamo estratto l'ID di sessione usando
string.find(file,''session_id=(%w+)''). Questa è una funzione molto importante nella libreria LUA e, anche se è descritta nel tutorial LUA su HTTP://lua-users.org, parleremo un po' di capture anche qui. Guardiamo i sorgenti della pagina. Ci interessa la riga
<a href="HTTP://localhost:3000/inbox.php?session_id=ABCD1234">inbox</a>
che contiene il session_id che vogliamo catturare. La nostra espressione è session_id=(%w+) che significa che vogliamo trovare tutte le stringhe che iniziano con session_id= e poi continuano con uno o più caratteri alfanumerici. Siccome abbiamo scritto %w+ tra parentesi tonde, intendiamo catturare il contenuto delle parentesi (la parte alfanumerica). Così string.find restituirà tre valori, i primi due sono ignorati (assegnati alla variabile dummy _) mentre il terzo è la stringa catturata (nel nostro caso ABCD1234). Il tutorial LUA su lua-users è molto ben fatto e su HTTP://sf.net/projects/lua-users potete trovare il LUA short reference che è un riassunto di tutte le funzioni standard LUA ed è anche un gran bel documento (mille grazie a Enrico Colombini). Se vi piace molto LUA dovreste comprare IL libro su LUA chiamato ``Programming in Lua'' di Roberto Ierusalimschy (consideratelo il K&R per LUA).

7.4.3 (step 3) Ottenere la lista dei messaggi

Ora dovremo implementare la funzione stat(). La stat è probabilmente la funzione più importante. Essa deve prelevare la lista dei messaggi nella webmail, il loro UIDL e la loro grandezza. Nel nostro esempio useremo il modulo mlex per tirare fuori le informazioni importanti dalla pagina, ma potete usare il modulo per le stringhe di LUA per fare la stessa cosa con i capture. Questa è la nostra pagina inbox (vedi figura 7)

Figure 7: inbox
\includegraphics[scale=0.8]{EPS/inbox.eps}

e questo è il corpo HTML (solo i primi due messaggi sono riportati)0.5

<h1>test@foo.xx - inbox (1/2)</h1> 

<form name="inbox" method="post" action="/delete.php"> 

<input type="hidden" name="session_id" value="ABCD1234"> 

<table> 

<tr><th>From</th><th>subject</th><th>size</th><th>date</th></tr> 

<tr>        

  <td><b>friend1@foo1.xx</b></td>         

  <td><b><a href="/read.php?session_id=ABCD1234&uidl=123">ok!</a></b></td>

  <td><b>20KB</b></td>

  <td><b>today</b></td>   

  <td><input type="checkbox" name="check_123"></td>

</tr> 

<tr>    

  <td>friend2@foo2.xx</td>        

  <td><a href="/read.php?session_id=ABCD1234&uidl=124">Re: hi!</a></td>  

  <td>12KB</td>   

  <td>yesterday</td>      

  <td><input type="checkbox" name="check_124"></td> 

</tr>

</table> 

<input type="submit" value="delete marked"> 

</form> 

<a href="/inbox.php?session_id=ABCD1234&page=2">go to next page</a> 

</body>

Abbiamo prelevato l'HTML usando il browser e il metodo get_uri() (ricordate che la URI per l'inbox era nella pagina di login). Come vedete i messaggi sono in una tabella, e tale tabella ha la stessa struttura per ogni messaggio. Proprio questo è il posto in cui usare mlex. Semplicemente, prendete tutto ciò che c'è tra <tr> e </tr> di una riga di un messaggio e cancellate tutto tranne i nomi dei i tag. Poi sostituite tutti gli spazi vuoti (chiameremo spazio la stringa tra due tag) con un``.*''. Ecco cosa abbiamo ottenuto (dovrebbe essere tutto sulla stessa riga, qui andiamo a capo per mancanza di spazio) dal primo messaggio.0.5

.*<tr>.*<td>.*<b>.*</b>.*</td>.*<td>.*<b>.*<a>.*</a>.*</b>.*</td>.*

<td>.*<b>.*</b>.*</td>.*<td>.*<b>.*</b>.*</td>.*

<td>.*<input>.*</td>.*</tr>

Questa espressione è usata per fare match con la riga della tabella che contiene informazioni sul messaggio. Ora copiate e incollate a parte la riga e sostituite ogni spazio e ogni tag con O (la lettera, non la cifra 0) o X. Mettete una X nei campi interessanti (nel nostro esempio la grandezza e il tag input, che contiene lo UIDL del messaggio).0.5

O<O>O<O>O<O>O<O>O<O>O<O>O<O>O<O>O<O>O<O>O<O>O

<O>O<O>X<O>O<O>O<O>O<O>O<O>O<O>O

<O>O<X>O<O>O<O>

Mentre la prima espressione verrà usata per fare match con la riga della tabella, questa verrà usata per estrarre i campi importanti. Questo codice lancia mlex sull'HTML e riempie la struttura dati popstate con i dati catturati.0.5

-- -------------------------------------------------------------------------- -- 

-- Fill the number of messages and their size 

function stat(pstate)

      local file,err = nil, nil

      local b = foo_globals.browser

      file,err = b:get_uri("http://localhost:3000/inbox.php?session_id="..

              foo_globals.session_id)

      local e = ".*<tr>.*<td>.*<b>.*</b>.*</td>.*<td>.*<b>.*<a>"..               

              ".*</a>.*</b>.*</td>.*<td>.*<b>.*</b>.*</td>.*<td>.*"..                

              "<b>.*</b>.*</td>.*<td>.*<input>.*</td>.*</tr>"         

      local g = "O<O>O<O>O<O>O<O>O<O>O<O>O<O>O<O>O<O>O<O>O<O>O"..              

              "<O>O<O>X<O>O<O>O<O>O<O>O<O>O<O>O<O>O<X>O<O>O<O>"

      local x = mlex.match(file,e,g) 

      --debug print   

      x:print()



      set_popstate_nummesg(pstate,x:count())

      for i=1,x:count() do            

              local _,_,size = string.find(x:get(0,i-1),"(%d+)")

              local _,_,size_mult_k = string.find(x:get(0,i-1),"([Kk][Bb])") 
	      
	      local _,_,size_mult_m = string.find(x:get(0,i-1),"([Mm][Bb])")

              local _,_,uidl = string.find(x:get(1,i-1),"check_(%d+)")

           

              if size_mult_k ~= nil then

                     size = size * 1024

              end             

              if size_mult_m ~= nil then

                     size = size * 1024 * 1024

              end             

     

              set_mailmessage_size(pstate,i,size)                     

              set_mailmessage_uidl(pstate,i,uidl)

      end

 

      return POPSERVER_ERR_OK

end

Il risultato di x:print() è il seguente0.5

{'20KB','input type="checkbox" name="check_123"'}

e la sessione di telnet0.5

+OK FreePOPs/0.0.11 pop3 server ready 

user test@foo.xx 

+OK PLEASE ENTER PASSWORD 

pass secret 

+OK ACCESS ALLOWED 

stat 

+OK 1 20480 

quit 

+OK BYE BYE, UPDATING

Non abbiamo indicato come abbiamo aggiunto la riga return POPSERVER_ERR_OK alla funzione quit(). Il codice sorgente riportato sopra usa mlex per estrarre le due stringhe interessanti, poi la scorre cercando la grandezza, il suo moltiplicatore e lo UIDL. Di seguito imposta gli attributi dei messaggi. Potete vedere che abbiamo processato solo il primo messaggio. Per processare gli altri dobbiamo informare il modulo mlex che il tag <b> è opzionale (potete notare che solo il primo messaggio è in grassetto). Quindi cambiamo le espressioni in0.5

.*<tr>.*<td>[.*]{b}.*{/b}[.*]</td>.*<td>[.*]{b}.*<a>.*</a>.*{/b}[.*]</td>.*

<td>[.*]{b}.*{/b}[.*]</td>.*<td>[.*]{b}.*{/b}[.*]</td>.*

<td>.*<input>.*</td>.*</tr>

e0.5

O<O>O<O>[O]{O}O{O}[O]<O>O<O>[O]{O}O<O>O<O>O{O}[O]<O>O

<O>[O]{O}X{O}[O]<O>O<O>[O]{O}O{O}[O]<O>O

<O>O<X>O<O>O<O>

Ora il comando stat risponde con +OK 4 45056 e la stampa di debug è 0.5

{'20KB','input type="checkbox" name="check_123"'} 

{'12KB','input type="checkbox" name="check_124"'} 

{'10KB','input type="checkbox" name="check_125"'} 

{'2KB','input type="checkbox" name="check_126"'}

Ora abbiamo una vera e propria funzione stat che riempie la struttura dati popstate con le informazioni di cui il server POP necessita per rispondere ad una richiesta di stat. poiché le richieste list, uidl, list_all e uidl_all possono essere soddisfatte con gli stessi dati, useremo la funzione standard fornita dal modulo common.lua. Esso verrà spiegato nel prossimo passo, ma dobbiamo aggiungere due righe importanti alla funzionestat() per evitare una doppia chiamata.0.5

function stat(pstate) 

       if foo_globals.stat_done == true then return POPSERVER_ERR_OK end



       ... the same code here ...



       foo_globals.stat_done = true

       return POPSERVER_ERR_OK

end

La funzione più importante è pronta, ma dobbiamo fare delle precisazioni. Primo, mlex è molto comodo a volte, ma potreste trovare più utile la libreria per le stringhe di LUA o la libreria regualerexp (espressioni regolari estese posix) per raggiungere lo stesso scopo. Secondo, questa implementazione si ferma alla prima pagina di inbox. Dovreste visitare tutte le pagine di inbox, forse usando la funzione do_until() nella libreria support.lua (che descriveremo brevemente alla fine di questo tutorial). Terzo, non facciamo nessun controllo degli errori. Per esempio la variabile file può essere nil e dobbiamo controllare queste cose per fare un buon plugin.

7.4.4 (step 4) Le funzioni comuni

Il modulo comune ci dà alcune funzioni precotte che dipendono solo da una stat() ben implementata (una stat che può essere chiamata più di una volta). Ecco la nostra implementazione di queste funzioni0.5

-- -------------------------------------------------------------------------- -- 

-- Fill msg uidl field 

function uidl(pstate,msg) return common.uidl(pstate,msg) end 



-- -------------------------------------------------------------------------- -- 

-- Fill all messages uidl field 

function uidl_all(pstate) return common.uidl_all(pstate) end 



-- -------------------------------------------------------------------------- -- 

-- Fill msg size 

function list(pstate,msg) return common.list(pstate,msg) end 



-- -------------------------------------------------------------------------- -- 

-- Fill all messages size 

function list_all(pstate) return common.list_all(pstate) end 



-- -------------------------------------------------------------------------- -- 

-- Unflag each message merked for deletion 

function rset(pstate) return common.rset(pstate) end



-- -------------------------------------------------------------------------- -- 

-- Mark msg for deletion 

function dele(pstate,msg) return common.dele(pstate,msg) end 



-- -------------------------------------------------------------------------- -- 

-- Do nothing 

function noop(pstate) return common.noop(pstate) end

ma prima aggiungete il codice per caricare il modulo comune alla vostra funzione init().0.5

        ... the same code ..



        -- the common module    

        require("common")



        -- checks on globals    

        freepops.set_sanity_checks()    



        return POPSERVER_ERR_OK 

end

7.4.5 (step 5) Cancellazione dei messaggi

La cancellazione di un messaggio è solitamente un normale POST e un esempio di post_data è session_id=ABCD1234&check_124=on&check_126=on. Il codice segue0.5

-- -------------------------------------------------------------------------- -- 

-- Update the mailbox status and quit 

function quit_update(pstate)

      -- we need the stat

      local st = stat(pstate)

      if st ~= POPSERVER_ERR_OK then return st end

         

      -- shorten names, not really important  

      local b = foo_globals.b         

      local post_uri = b:wherearewe() .. "/delete.php"        

      local session_id = foo_globals.session_id       

      local post_data = "session_id=" .. session_id .. "&"

    

      -- here we need the stat, we build the uri and we check if we   

      -- need to delete something     

    

      local delete_something = false; 

      for i=1,get_popstate_nummesg(pstate) do                 

             if get_mailmessage_flag(pstate,i,MAILMESSAGE_DELETE) then                                            
	     	post_data = post_data .. "check_" .. 

                        get_mailmessage_uidl(pstate,i).. "=on&"                             

                delete_something = true                 

             end     

      end

    

      if delete_something then                

             b:post_uri(post_uri,post_data)  

      end

    

      return POPSERVER_ERR_OK 

end

Considerate che facciamo il POST solo se almeno un messaggio è segnato per la cancellazione. Un'altra cosa importante da tenere a mente è che fare un solo POST per tutti i messaggi è meglio che farne uno per ognuno. Quando possibile dovreste ridurre il numero di richieste HTTP al massimo dato che è qui che portiamo FreePOPs da lepre a tartaruga.

7.4.6 (step 6) Scaricare messaggi

Potrete chiedervi perché parliamo di questo argomento solo al punto 6, d'altronde avere la posta è probabilmente ciò che volete da un plugin. Implementare la funzione retr() è di solito facile. Dipende in realtà dalla webmail, ma qui parleremo del caso semplice, mentre alla fine del tutorial vedrete come gestire webmail complesse. Il caso base è quello in cui la webmail ha un pulsante per salvare i messaggi, e il messaggio salvato è un file di testo semplice che contiene sia l'header che il corpo del messaggio. Ci sono solo due questioni interessanti in questo caso, e cioè quelle relativa ai messaggi grandi al punto.

I messaggi grandi causano timeout. Sì, il modo più semplice di scaricare un messaggio è chiamare b:get_uri() e memorizzare il messaggio in una variabile, poi mandarlo al client di posta con popserver_callback(). Ma pensate che una mail da 5MB, scaricata con una connessione DSL da 640Kbps, alla piena velocità di 80KBps, impiega 64 secondi di download. Questo significa che il vostro plugin non manderà dati al client di posta per oltre un minuto, facendo sì che il client si disconnetta da FreePOPS pensando che il server POP3 sia morto. Per cui, dobbiamo mandare dati al client di posta appena possibile. Per questo abbiamo la funzione b:pipe_uri() che chiama un callback ogni volta che ha dei dati freschi. Il codice seguente è la funzione di callback factory, che crea un nuovo callback da passare al metodo pipe_uri del browser.0.5

-------------------------------------------------------------------------------- 

-- The callback factory for retr 

-- 

function retr_cb(data)

        local a = stringhack.new() 

        return function(s,len)

                s = a:dothack(s).."\0"          

                popserver_callback(s,data)              

                return len,nil

        end

end

Qui potete vedere che il callback usa popserver_callback() per passare dati al client di posta, ma prima di fare ciò manipola i dati con lo stringhack. Ma questa è la seconda questione interessante.

Il protocollo POP3 deve terminare la risposta al comando retr con una riga che contiene solo tre byte, ``.\r\n''. Ma che succede se una riga, dentro il corpo della mail, è un semplice punto? Dobbiamo cambiarlo in ``..\r\n''. Non è cosi' difficile, una string.gsub(s,''\r\n.\r\n'',''\r\n..\r\n'') è tutto ciò che ci serve... ma non nel caso dei callback. Il callback di invio verrà chiamato con dati freschi, e più di una volta se la mail è grande. E se il pattern cercato è troncato tra due chiamate il metodo string.gsub() fallirà. Per questo il modulo stringhack ci viene incontro. L'oggetto a vive fintantoché la funzione di callback viene chiamata (vedi il tutorial LUA) e terrà a mente che il pattern cercato può essere troncato.

Infine, il codice della retr().0.5

-- -------------------------------------------------------------------------- -- 

-- Get message msg, must call  

-- popserver_callback to send the data 

function retr(pstate,msg,pdata)          

        -- we need the stat

        local st = stat(pstate)

        if st ~= POPSERVER_ERR_OK then return st end 

    

        -- the callback

        local cb = retr_cb(data) 

     

        -- some local stuff

       local session_id = foo_globals.session_id

       local b = internal_state.b

       local uri = b:wherearewe() .. "/download.php?session_id="..session_id..

                "&message="..get_mailmessage_uidl(pstate,msg) 



        -- tell the browser to pipe the uri using cb

        local f,rc = b:pipe_uri(uri,cb)

        if not f then

                log.error_print("Asking for "..uri.."\n")

                log.error_print(rc.."\n")

                return POPSERVER_ERR_NETWORK

        end

end

7.4.7 (step 7) Test

Per fare un buon plugin ci vuole un sacco di testing. Dovreste cercare beta tester presso il forum di FreePOPs (HTTP://freepops.diludovico.it) e chiedere agli autori del software di includerlo nella distribuzione principale. Dovreste anche leggere il contratto della webmail, controllare se c'è qualcosa come ``Non userò mai un server webmail->pop3 per leggere la mia posta'' e inviare una copia agli autori del software.

7.4.8 (step 8) La tanto anticipata parte finale del tutorial

Ci sono un sacco di cose che abbiamo tralasciato.

La multi-page stat
è la vera buona implementazione per stat(). Abbiamo detto sopra che la nostra implementazione elenca solo i messaggi nella prima pagina. Il codice per il parsing e l'estrazione di informazioni interessanti da una pagina è già scritto, ci serve solo una funzione che controlli se siamo all'ultima pagina e se no cambi il valore di una variabile uri. La variabile uri in questione sarà usata dalla funzione di prelevamento. In questo caso dovreste usare il modulo di supporto con il ciclo do_until. Questo è un semplice esempio di do_until() 0.5

-- -------------------------------------------------------------------------- -- 

-- Fill the number of messages and their size 

function stat(pstate)

        ... some code as before ...



        -- this string will contain the uri to get. it may be updated by        

        -- the check_f function, see later      

        local uri = string.format(libero_string.first,popserver,session_id)



        -- The action for do_until      

        --      

        -- uses mlex to extract all the messages uidl and size  

        local function action_f (s)

                 -- calls match on the page s, with the mlexpressions

                 -- statE and statG              

                 local x = mlex.match(s,e,g)                 

               

                 -- the number of results                

                 local n = x:count()

                

                 if n == 0 then return true,nil end

 

                 -- this is not really needed since the structure                

                 -- grows automatically... maybe... don't remember now 

                 local nmesg_old = get_popstate_nummesg(pstate)

                 local nmesg = nmesg_old + n 

                 set_popstate_nummesg(pstate,nmesg)

      

                 -- gets all the results and puts them in the popstate structure                              for i = 1,n do                        

                         ... some code as before ...

 

                         set_mailmessage_size(pstate,i+nmesg_old,size)                     

                         set_mailmessage_uidl(pstate,i+nmesg_old,uidl)           

                 end     

                

                 return true,nil         

        end

        

        -- check must control if we are not in the last page and        

        -- eventually change uri to tell retrive_f the next page to retrive     

        local function check_f (s)              

                 local tmp1,tmp2 = string.find(s,next_check)              

                 if tmp1 ~= nil then                     

                          -- change retrive behaviour                     

                          uri = "--build the uri for the next page--"



                          -- continue the loop

                          return false       

                 else

                          return true

                 end

        end



        -- this is simple and uri-dependent

        local function retrive_f ()

                 local f,err = b:get_uri(uri)

                 if f == nil then 

                         return f,err

                 end

      

                 local _,_,c = string.find(f,"--timeout string--")

                 if c ~= nil then

                         internal_state.login_done = nil                                

                         session.remove(key())

                         local rc = libero_login()                       

                         if rc ~= POPSERVER_ERR_OK then                          

                                 return nil,"Session ended,unable to recover"                                         end             

                        

                         uri = "--uri for the first page--"      

                         return b:get_uri(uri)           

                  end     

               

                  return f,err    

        end



        -- initialize the data structure

        set_popstate_nummesg(pstate,0)

 

        -- do it        

        if not support.do_until(retrive_f,check_f,action_f) then

                  log.error_print("Stat failed\n")

                  session.remove(key())           

                  return POPSERVER_ERR_UNKNOWN    

        end

        

        -- save the computed values     

        internal_state["stat_done"] = true 

        return POPSERVER_ERR_OK 

end

Le uniche cose strane sono la funzione di prelevamento e quel che serve per salvare la sessione. Dato che le webmail a volte fanno timeout dovreste controllare se la pagina prelevata sia valida o no, ed eventualmente ritentare il login. Il salvataggio della sessione è la prossima questione.

Salvare la sessione
è il modo per rendere FreePOPs davvero simile ad un browser. ciò significa che la prossima volta che controllate la posta FreePOPs ricaricherà semplicemente la pagina inbox senza rifare il login. Per fare questo avete bisogno di una funzione key() che crea un ID unico per ogni sessione0.5

-------------------------------------------------------------------------------- 

-- The key used to store session info 

-- 

-- This key must be unique for all webmails, since the session pool is one  

-- for all the webmails 

-- 

function key()

        return foo_globals.username .. foo_globals.password

end

e una funzione di serializzazione foo_globals0.5

-------------------------------------------------------------------------------- 

-- Serialize the internal state 

-- 

-- serial.serialize is not enough powerful to correcly serialize the  

-- internal state. The field b is the problem. b is an object. This means 

-- that it is a table (and no problem for this) that has some field that are 

-- pointers to functions. this is the problem. there is no easy way for the  

-- serial module to know how to serialize this. so we call b:serialize  

-- method by hand hacking a bit on names 

-- 

function serialize_state()   

        internal_state.stat_done = false; 

        return serial.serialize("foo_globals",foo_globals) ..            

                internal_state.b:serialize("foo_globals.b") 

end

Ora dovete dire a FreePOPs di salvare lo stato nella funzione quit_update() e caricarlo nella pass(). Questa è la nuova struttura pass()0.5

function pass(pstate,password)  

        -- save the password

        internal_state.password = password



        -- eventually load session

        local s = session.load_lock(key())



        -- check if loaded properly

        if s ~= nil then

                 -- "\a" means locked

                 if s == "\a" then

                          log.say("Session for "..internal_state.name..

                              " is already locked\n")

                          return POPSERVER_ERR_LOCKED

                 end 

     

                 -- load the session

                 local c,err = loadstring(s)

                 if not c then

                          log.error_print("Unable to load saved session: "..err)

                          return foo_login()

                 end     

       

                 -- exec the code loaded from the session string

                 c()



                log.say("Session loaded for " .. internal_state.name .. "@" ..

                         internal_state.domain ..

                         "(" .. internal_state.session_id .. ")\n")      



                return POPSERVER_ERR_OK

        else

                -- call the login procedure

                return foo_login()

        end

end

dove foo_login() è la vecchia funzione pass() con cambiamenti minori. Non dimenticate di chiamare session.unlock(key()) nella funzione quit(), perché dovrete rilasciare la sessione in caso di fallimento (e quit() viene chiamata qui) e salvare la sessione in quit_update()0.5

-- save fails if it is already saved  

session.save(key(),serialize_state(),session.OVERWRITE)

-- unlock is useless if it have just been saved, but if we save

-- without overwriting the session must be unlocked manually

-- since it would fail instead overwriting

session.unlock(key())

La funzione top()
è piuttosto complessa. Non la descriveremo in modo completo, ma suggeriamo di guardare il plugin libero.lua se il server web che vi manda i messaggi supporta il campo ``Range:'' nelle richieste HTTP, o il plugin HTTP request field, o il plugin tin.lua se il server deve essere interrotto in malo modo. Ricordate che la top() ha bisogno che qualcuno conti le righe e qui abbiamo di nuovo il modulo stringhack, che conta ed eventualmente elimina delle righe.
Il javascript
è l'inferno delle webmail. I Javascript possono fare qualsiasi cosa e dovrete leggerli per emulare ciò che fanno. Per esempio potrebbero aggiungere alcuni cookie (e dovrete fare lo stesso a mano con b:add_cookie() come in tin.lua) oppure possono cambiare alcuni campi di form (come nel codice di bilanciamento del carico in libero.lua).
I cookie
sono abbastanza appetibili per noi, visto che il modulo browser se ne occupa al posto nostro.
I file standard
sono decisamente dipendenti dal sistema. Sotto Windows dovrete costantemente guardare stderr.txt e stdout.txt, mentre sotto Unix dovrete solo lanciare FreePOPs con il parametro -w e guardare la console.
La forza bruta
si chiama Ethereal. A volte le cose non funzionano nel modo giusto e l'unico modo per fare debug è attivare curl debugging per vedere cosa fa FreePOPs (b.curl:setopt(curl.OPT_VERBOSE,1)) e sniffare ciò che fa un vero browser con un tool come, appunto, Ethereal.
Il modo open source
è il modo migliore di avere software di buona qualità. Questo significa che dovrete rilasciare molto spesso il vostro plugin nella fase di sviluppo e interagire molto con i vostri tester. Fidatevi, funziona, o leggete ``The cathedral and the bazaar'' di Eric Raymond.
Il modulo mimer
è molto beta mentre scriviamo queste righe, ma è ciò di cui avete bisogno se siete nel caso sfortunato di una webmail che non ha un pulsante per salvare i messaggi. Il plugin lycos.lua è un esempio di cosa può fare. La principale funzione interessante è mimer.pipe_msg() che prende un header di messaggio, il testo del corpo (in html o testo semplice) e gli URI di alcuni attachment, scaricati al volo, composti in una vera e propria mail che viene inoltrata al client di posta.
Parametri per i moduli
possono venire passati dal file config.lua o al volo usando user@domain?param1=value1&...&paramN=valueN come descritto nel capitolo sui plugin. Per l'autore di plugin non c'è differenza tra i due metodi. I parametri sono disponibili per il plugin nella tabella freepops.MODULE_ARGS.
Regex per definire i domini gestiti
sono supportate dalla version 0.0.29. I plugin ufficiali possono avere una riga nel config.lua come questa
freepops.MODULES_MAP["foo2.*"]  = {

name="foo.lua",

regex = true -- enables the regex processing

}
mentre i plugin non ufficiali possono dichiarare una lista di regex nel campo PLUGIN_REGEXES. Per esempio
PLUGIN_REGEXES = {"@foo2.*", "@foo3.*", "@foo4[A-Z]*"}


next up previous contents
Next: 8 Segnalare un bug Up: Manuale di FreePOPs Previous: 6 Plugin   Contents
Enrico Tassi 2008-11-01