next up previous contents
Next: 8 Submitting a bug Up: FreePOPs Manual Previous: 6 Plugins   Contents

Subsections

7 Creating a plugin

Two sections follow, the first is a quick overview of what a plugin has to do, the latter is a more detailed tutorial. Before proceeding I suggest you read some stuff that is at the base of plugin writing:

  1. Since plugins are written in LUA you must read at least the LUA tutorial (HTTP://lua-users.org/wiki/LuaTutorial); many thanks to the guys who wrote it. LUA is a quite simple scripting language, easy to learn, and easy to read. If you are interested in this language you should read THE book about LUA (``Programming in Lua'' by Roberto Ierusalimschy HTTP://www.inf.puc-rio.br/~roberto/book/). It is a really good book, believe me. Today I've seen that the book is completely available online here. HTTP://www.lua.org/pil/
  2. Since we have to implement a POP3 backend you should know what POP3 is. The RFC is number 1939 and is included in the doc/ directory of the source package of FreePOPs, but you can fetch it from the net HTTP://www.ietf.org/rfc/rfc1939.txt.
  3. Read carefully this tutorial, it is hardly a good tutorial, but is better than nothing.
  4. The website contains, in the doc section, a quite good documentation of the sources. You should keep a web browser open at the LUA modules documentation page while writing a plugin.
  5. After creating a prototype, you should read a full featured plugin. The libero.lua plugin is really well commented, you may start there.
  6. Remember that this software has an official forum (HTTP://freepops.diludovico.it) and some authors you may ask for help.
  7. FreePOPs is licensed under the GNU/GPL license, therefore any and all software that makes use of its code must be released under the same license. This includes plugins. For more information read the enclosed COPYING file or see the license text at HTTP://www.gnu.org/licenses/gpl.html.

7.1 Plugins overview

A plugin is essentially a backend for a POP3 server. The plugins are written in LUA1 while the POP3 server is written in C. Here we examine the interfaces between The C core and the LUA plugins.

7.1.1 The interface between the C core and a plugin

As we explained before the C POP3 frontend has to be attached to a LUA backend. The interface is really simple if you know the POP3 protocol. Here we only summarize the meaning, but the RFC 1939 (included in the doc/ directory of the source distribution) is really short and easy to read. As your intuition should suggest the POP3 client may ask the pop3 server to know something about the mail that is in the mailbox and eventually retrieve/delete a message. And this is exactly what it does.

The backend must implement all the POP3 commands (like USER, PASS, RETR, DELE, QUIT, LIST, ...) and must give back to the frontend the result. Let us give a simple example of a POP3 session taken from the 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 password

     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 this session the backend will be called for lines 4, 6, 8, 10, 14, 18, 20 (all the C: lines) and respectively the functions implementing the POP3 commands will be called this way

0.5

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

    pass(p,"gpl")

    stat(p)

    list_all(p)

    retr(p,1)

    dele(p,1)

    quit_update(p)

Later I will make clear what p is. I hope we'll remove it making it implicit for complete transparency. It is easy to understand that there is a 1-1 mapping between POP3 commands and plugin function calls. You can view a plugin as the implementation of the POP3 interface.

7.1.2 The interface between a plugin and the C core

Let us take in exam the call to pass(p,''gpl''). Here the plugin should authenticate the user (if there is a sort of authentication) and inform the C core of the result. To achieve this each plugin function must return an error flag, to be more accurate one of these errors:

Code Meaning
POPSERVER_ERR_OK


In our case the most appropriate error codes are POPSERVER_ERR_AUTH and POPSERVER_ERR_OK. This is a simple case, in which an error code is enough. Now we analyze the more complex case of the call to list_all(p). Here we have to return an error code as before, but we also have to inform the C core of the size of all messages in the mailbox. We need the p parameter passed to each plugin function (note that that parameter may became implicit in the future). p stands for the data structure that the C core expects us to fill calling appropriate functions like set_mailmessage_size(p,num,size) where num is the message number and size is the size in bytes. Usually it is really common to put more functions all together. For example when you get the message list page in a webmail you know the number of the messages, their size and uidl so you can fill the p data structure with all the informations for LIST, STAT, UIDL.

The last case that we examine is retr(p,num,data). Since a mail message can be really big, there is no pretty way of downloading the entire message without making the mail client complain about the server death. The solution is to use a callback. Whenever the plugin has some data to send to the client he should call the popserver_callback(buffer,data). data is an opaque structure the popserver needs to accomplish its work (note that this parameter may be removed for simplicity). In some cases, for example if you know the message is small or you are working on a fast network, you can fetch the whole message and send it, but remember that this is more memory consuming.

7.2 The art of writing a plugin (plugins tutorial)

In this section we will write a plugin step by step, examining each important detail. We will not write a real and complete plugin since it may be a bit hard to follow but we will create an ad-hoc webmail for our purposes.

7.2.1 (step 1) The skeleton

The first thing we will do is copy the skeleton.lua file to foo.lua (since we will write the plugin for the foo.xx webmail, xx stands for a real domain, but I don't want to mention any websites here...). Now with your best editor (I suggest vim under Unix and scintilla for win32, since they support syntax highlights for LUA, but any other text editor is OK) open foo.lua and change the first few lines adding the plugin name, version, your name, your email and a short comment in the proper places.0.5

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

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

--  

--  $Id: manual.tex,v 1.41 2007/10/28 12:32:22 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--"

Now we have an empty plugin, but it is not enough to start hacking on it. We need to open the config.lua file (in the win32 distribution it is placed in the main directory, while in the Unix distribution it is in /etc/freepops/; other copies of this file may be included in the distributions, but they are backup copies) and add a line like this0.5

-- foo plugin 

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

at the beginning of the file. Before ending the first step you should try if the plugin is correctly activated by FreePOPs when needed. To do this we have to add few lines to foo.lua, in particular we have to add an error return value to user().0.5

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

-- Must save the mailbox name 

function user(pstate,username)   

        return POPSERVER_ERR_AUTH 

end

Now the user function always fails, returning an authentication error. Now you have to start FreePOPs (if it is already running you don't have to restart it) and start telnet (under win32 you should open a DOS prompt, under Unix you have the shell) and type telnet localhost 2000 and then type 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.

The server responds closing the connection and printing an authorization failed message (thats OK, since the user() function of our plugin returns this error). In the standard error file (the console under Unix, the file stderr.txt under Windows) the error messages get printed, don't mind them now.

7.2.2 (step 2) The login

The login procedure is the first thing we have to do. The POP3 protocol has 2 commands for login, user and pass. First the client does a user, then it tells the server the password. As we have already seen in the overview this means that first user() and thenpass() will be called. This is a sample 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

If you start FreePOPs with the -w switch you should read this on 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

and the plugin has been changed a bit to store the user login and print some debug info. This is the plugin that gave this 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

Here we have some important news. First the foo_globals table that will contain all the globals (values that should be available to successive function calls) we need. So far we have the username and password there. The user() function now stores the passed username in the foo_globals table and prints something on standard output. The pass() function likewise stores the password in the global table and prints some stuff. The quit() function simply returns POPSERVER_ERR_OK to make FreePOPs happy.

Now that we know how FreePOPs will act during the login we have to implement the login in the webmail, but first uncomment the few lines in the init() function (that is called when the plugin is started), that loads the browser.lua module (the module we will use to login in the webmail). Here is the webmail login page viewed with Mozilla and the source of the page (you can see it with Mozilla with Ctrl-U, figure 5).

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

0.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>

Here we have 2 input fields, one called username and one called password. When the user clicks login the web browser will POST to HTTP://localhost:3000/ the form contents (I used a local address for comfort, but it should be something like HTTP://webmail.foo.xx/login.php). This is what the browser sends: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 

We are not interested in the first part (the HTTP header, since the browser module will take care of it) but in the last part, the posted data. Since the fields of the form were username and password, the posted data is 
username=test%40foo.xx&password=hello. Now we want to reproduce the same HTTP request with our plugin. This is the simple code that will do just that.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

First we create a browser object, then we build the post_uri and post_data using a simple string.format (printf-like function). And this is the resulting request0.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

that is essentially the same (we should url-encode the post data with curl.escape()) we wanted to do. We saved the browser object to the global table, since we want to use the same browser all the time.

Now that we have logged in, we want to check the resulting page, and maybe extract a session ID that will be used later. This is the code to extract the session id and the HTML page we have received in response to the login request0.5

 

        ... the same as before here ...

     

        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

and figure 6 is the returned web page.

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>

Note that we extracted the session ID string using
string.find(file,''session_id=(%w+)''). This is a really important function in the lua library and, even if it is described in the lua tutorial at HTTP://lua-users.org, we will talk a bit about captures here. Look at the page source. We are interested in the line
<a href="HTTP://localhost:3000/inbox.php?session_id=ABCD1234">inbox</a>
that contains the session_id we want to capture. Our expression is session_id=(%w+) that means we want to match all the strings that start with session_id= and than continue with one or more alphanumerical character. Since we wrote %w+ in round brackets, we mean to capture the content of brackets (the alphanumerical part). So string.find will return 3 values, the first two are ignored (assigned to the dummy variable _) while the third is the captured string (in our case ABCD1234). The LUA tutorial at lua-users is quite good and at HTTP://sf.net/projects/lua-users you can find the LUA short reference that is a summary of all standard lua functions and is a really good piece of paper (so many thanks to Enrico Colombini). If you really like LUA you should buy THE book about LUA called ``Programming in Lua'' by Roberto Ierusalimschy (consider it the K&R for LUA).

7.2.3 (step 3) Getting the list of messages

Now we have to implement the stat() function. The stat is probably the most important function. It must retrieve the list of messages in the webmail and their UIDL and size. In our example we will use the mlex module to grab the important info from the page, but you can use the string LUA module to do the same with captures. This is our inbox page (see figure 7)

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

and this is the HTML body (only the first 2 messages are reported)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>

We have retrieved the HTML using the browser and the get_uri() method (remember the URI for the inbox was in the login page). As you can see the messages are in a table and this table has the same structure for each message. This is the place in which you may use mlex. Just take all the stuff between <tr> and </tr> of a message row and delete all but the tags name. Then replace every empty space (we call space the string between two tags) with a ``.*''. This is what we have obtained (it should be all in the same line, here is wrapped for lack of space) from the first message.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>

This expression is used to match the table row containing info about the message. Now cut and paste the line and replace every space and every tag with O (the letter, not the digit 0) or X. Put an X in the interesting fields (in our example the size and the input tag, that contains the message uidl).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>

While the first expression will be used to match the table row, this one will be used to extract the important fields. This is the code that starts mlex on the HTML and fills the popstate data structure with the captured data.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

The result of x:print() is the following0.5

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

and the telnet session follows0.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

We have not listed here how we added the dummy return POPSERVER_ERR_OK line to the quit() function. The source code listed before uses mlex to extract the two interesting strings, then parses them searching for the size and the size multiplier and the uidl. Then sets the mail message attributes. But here you can see that we just matched the first message. To match the other messages we have to inform the mlex module that the <b> tag is optional (you can see that only the first message is in bold). So we change the expressions to0.5

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

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

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

and0.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>

Now the stat command responds with +OK 4 45056 and the debug print is 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"'}

Now we have a proper function stat that fill the popstate data structure with the info the popserver needs to respond to a stat request. Since the list, uidl, list_all and uidl_all requests can be satisfied with the same data we will use the standard function provided by the common.lua module. It will be explained in the next step, but we have to add 2 important lines to the stat() function, to avoid a double call.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

The most important function is done, but a lot of notes must be written here. First, mlex is really comfortable sometimes, but you may find more helpful using the lua string library or the regularexp library (posix extended regular expressions) to reach the same point. Second, this implementation stops at the first inbox page. You should visit all the inbox pages maybe using the do_until() function in the support.lua library (that will be briefly described at the end of this tutorial). Third we make no error checking. For example the file variable may be nil and we must check these things to make a good plugin.

7.2.4 (step 4) The common functions

The common module gives us some pre-cooked functions that depend only on a well implemented stat() (I mean a stat than can be called more than once). This is our implementation of these functions 0.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 marked 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

but first add the common module loading code to your init() function0.5

        ... the same code ..



        -- the common module    

        require("common")



        -- checks on globals    

        freepops.set_sanity_checks()    



        return POPSERVER_ERR_OK 

end

7.2.5 (step 5) Deleting messages

Deleting messages is usually a normal post and an example of the post_data is session_id=ABCD1234&check_124=on&check_126=on. The code follows0.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

Consider we do the post only if at least one message is marked for deletion. Another important think to keep in mind is that making only one post for all messages is better than making a single post for each message. When it is possible you should reduce the number of HTTP requests as much as you can since it is here we move FreePOPs from a rabbit to a tortoise.

7.2.6 (step 6) Downloading messages

You may ask why I talk about this only at point 6, while having the mail is probably what you want from a plugin. Implementing the retr() function is usually simple. It really depends on the webmail, but here we will talk of the simple case, while at the end of the tutorial you will see how to deal with complex webmails. The simple case is the one in which the webmail has a save message button. And the saved message is a plain text file containing both the header and the body of the message. There are only two interesting points in this case, firstly big messages, secondly the dot issue.

Big messages are a cause of timeout. Yes, the most simple way of downloading a message is calling b:get_uri() and store the message in a variable, and then send it to the mail client with popserver_callback(). But think that a 5MB mail, downloaded with a 640Kbps DSL connection, at full 80KBps speed, takes 64 seconds to download. This means your plugin will not send any data to the mail client for more than one minute and this will make the mail client to disconnect from FreePOPs thinking the POP3 server is dead. So we must send the data to the mail client as soon as we can. For this we have the b:pipe_uri() function that calls a callback whenever it has some fresh data. The following code is the callback factory function, that creates a new callback to pass to the pipe_uri browser method.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

Here you see that the callback simply uses popserver_callback() to pass the data to the mail client, but before doing this it mangles the data with the stringhack. But this is the second interesting point.

The POP3 protocol should end the retr command answer with a line that contains only 3 bytes, ``.\r\n''. But what if a line, inside the mail body, is a simple point? We have to escape it to ``..\r\n''. This is not so hard, a string.gsub(s,''\r\n.\r\n'',''\r\n..\r\n'') is all we need... but not in the case of callbacks. The send callback will be called with some fresh data, and called more than once if the mail is big. And if the searched pattern is truncated between two calls the string.gsub() method will fail. This is why the stringhack module helps us. The a object lives as long as the callback function will be called (see the closure page of the lua tutorial) and will keep in mind that the searched pattern may be truncated.

Finally the retr() code0.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.2.7 (step 7) Test it

Making a good plugin needs a lot of testing. You should ask for beta testers at the FreePOPs forum (HTTP://freepops.diludovico.it) and ask the software authors to include it in the main distribution. You should also read the webmail contract, check if there is something like ``I'll never use webmail->pop3 server to read my mail'' and send a copy to the authors of the software.

7.2.8 (step 8) The so mentioned last part of the tutorial

There are a lot of things we have omitted here.

The multi-page stat
is the real good implementation for stat(). We mentioned before that our implementation lists only the messages in the first page. The code for parsing and extracting interesting info from a page is already written, we simply need a function that checks if we are in the last page and if not it changes the value of a uri variable. The uri variable will be used by the fetch function. In this case you should use the support module with the do_until cycle. This is a simple example of 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 retrieve_f the next page to retrieve     

        local function check_f (s)              

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

                 if tmp1 ~= nil then                     

                          -- change retrieve behavior                     

                          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 retrieve_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(retrieve_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

The only strange things are the retrieve function and the session saving stuff. Since webmail sometimes timeout you should check if the retrieved page is valid or not, and eventually retry the login. The session saving is the next issue.

Saving the session
is the way to make FreePOPs really similar to a browser. This means the next time you check the mail FreePOPs will simply reload the inbox page and won't login again. To do this you need a key() function that gives a unique ID for each session0.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

and a foo_globals serialization function0.5

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

-- Serialize the internal state 

-- 

-- serial.serialize is not enough powerful to correctly 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

Now you have to tell FreePOPs to save the state in the quit_update() function and load it back in the pass() one. This is the new pass() structure0.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

where foo_login() is the old pass() function with minor changes. Don't forget to call session.unlock(key()) in the quit() function, since you have to release the session in case of failure (and quit() is called here) and to save the session 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())

The top() function
is a complex thing. I won't describe it in a complete way, but I suggest you to look at the libero.lua plugin if the web server that sends you the message source supports the ``Range:'' HTTP request field, or the tin.lua plugin if the server needs to be interrupted in a bad way. Remember that the top() needs someone that counts the lines and here we have again the stringhack module that counts and may purge some lines.
The javascript
is the hell of webmails. Javascripts can do anything and you have to read them to emulate what they do. For example they may add some cookies (and you'll have to do this by hand with the b:add_cookie() as in tin.lua) or they may change some form fields (like in the libero.lua login load balancing code).
The cookies
are sweet enough for us, since the browser module will handle them for us.
The standard files
are really system dependent. Under Windows you'll have to constantly look at the stderr.txt and stdout.txt, while under Unix you will just have to start it with the -w switch and look at the console.
The brute force
is called Ethereal. Sometimes things don't work in the right way and the only way to debug them is to activate curl debugging to see what FreePOPs does (b.curl:setopt(curl.OPT_VERBOSE,1)) and sniff what a real browser does with a tool like Ethereal.
The open source way
is the best way of having a good quality piece of software. This means you'll have to release really often your plugin in the development phase and interact much with your testers. Trust me it works, or read ``The cathedral and the bazaar'' by Eric Raymond.
The mimer module
is really beta at the time of this tutorial, but is what you need if you are in the unlucky case of a webmail that has no save message button. The lycos.lua plugin is an example of what it can do. The main interesting function is mimer.pipe_msg() that takes a message header, a text body (in html o plain text format) and some attachments URIs, that are downloaded on the fly, composed into a proper mail message and piped to the mail client.
Parameters to modules
may be passed from the config.lua file or on the fly using user@domain?param1=value1&...&paramN=valueN as described in the plugins chapter. For the plugin writer there is no difference between the two passing mechanisms. The parameters are available to the plugin in the table freepops.MODULE_ARGS.
Regex to define handled domains
are allowed since version 0.0.29. Official plugins can have a config.lua line like
freepops.MODULES_MAP["foo2.*"]  = {

name="foo.lua",

regex = true -- enables the regex processing

}
while unofficial plugins can declare a regexp list in the PLUGIN_REGEXES field. For example
PLUGIN_REGEXES = {"@foo2.*", "@foo3.*", "@foo4[A-Z]*"}


next up previous contents
Next: 8 Submitting a bug Up: FreePOPs Manual Previous: 6 Plugins   Contents
Enrico Tassi 2008-11-01