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:
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.
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.
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.
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.
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.
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).
<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).
<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).
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)
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.
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
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.
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
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.
Ci sono un sacco di cose che abbiamo tralasciato.
-- -------------------------------------------------------------------------- --
-- 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.
--------------------------------------------------------------------------------
-- 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())
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]*"}