📜 ⬆️ ⬇️

FORTH: POP3 nanoclient. Part 2

In the second part we will try to write a minimal POP3 client. He will be able to connect to the server, log in to the mailbox, find out how many letters there are and download the latest. To illustrate this will be enough.

That it is enough to know from the POP3 protocol. A couple of USER PASS commands are needed for the login, STAT to get the number of letters in the box and RETR to get the letter itself. We will not delete letters with the DELE command. QUIT team will be given out of courtesy.
Let's take a closer look at the teams. You can see that they can be divided into two groups: commands without arguments NOOP QUIT STAT RSET and commands that have an argument: DELE LIST RETR TOP APOP USER PASS. And the last three require text strings as an argument. Strictly speaking, all commands require a string argument. In one case, this is an empty string, in another the string consists of decimal digits, in the third of arbitrary characters.

Consider the simplest case, argumentless commands. You can write in the forehead

  S "NOOP" sockt WriteSocketLine THROW and so 4 times. 
 S "DELE" sockt WriteSocket THROW S> D (D.) sockt WriteSocketLine THROW 
also 3 times
')
(TOP APOP will not be implemented, and with USER PASS we will not be wise yet.)


But we write on the Fort, and it has an elegant mechanism for creating defining words. We use it.
To begin with, we put the sockt WriteSocketLine THROW and buf bufsize sockt ReadSocket THROW TO bytes_read into separate definitions. Too lazy to write so many letters.
 :> sockt   
               sockt WriteSocketLine THROW;

 : sockt>
               buf bufsize sockt ReadSocket THROW TO bytes_read;



And we will start creating creating words.

 : pop3_noarg 
                      CREATE LATEST, 
                      DOES> @ COUNT> sockt; 

 : pop3_noargs 
                     0 DO pop3_noarg LOOP;

 4 pop3_noargs NOOP QUIT STAT RSET 


Thus, in three lines, we identified four teams of the same type. Let me explain a little more in detail what is happening here.
A colon is used to define a new word pop3_noarg, with the following action: select letters from the input stream, delimited by a space, use them to create words with VARIABLE on the top of the dictionary. This all makes the word CREATE, at the time of execution of the word pop3_noarg. The next word LATEST pushes onto the stack the address of the name field of the word being defined, for example NOOP. A comma compiles values ​​from the stack to the top of the dictionary. If there were no DOES> part further, a NOOP call would put some address onto the stack, dereferencing which we would get the address of the line with the counter, turning that into the address of the line counter with the word COUNT we could type it with the word TYPE.
As a result, they would see NOOP. Words look very difficult, faster to try on your own at the command line of the fort system.
What next? Next we have the most mysterious word of the Fort DOES>. It is enough to realize that this word changes the default action of a word created through CREATE into an action that we wish. Everything that is written after DOES> is a common action for all words defined via pop3_noarg. Here it consists in getting the address and the string counter - the name of the word and sending it to the socket.

  : pop3_digiarg 
                CREATE LATEST, 
                DOES> @> R <# S> D #S BL HOLD R> COUNT HOLDS #>> sockt;

 : pop3_digiargs 0 DO pop3_digiarg LOOP;

 3 pop3_digiargs DELE LIST RETR 


Very similar to the previous group of words with a noticeable difference in the DOES> part. It is clear that something is sent to the socket. And so what?
Here we use another great opportunity of the Fort. We have a beautiful word (D.) which converts a double precision number (64 bits) from stack to line. And we could easily use it, but for this we would have to write something like

 S "LIST" sockt WriteSocket S> D (D.)> sockt 


Not scary, but somehow sad.

Since we decided to make it beautiful, why not look for other solutions. If you look at the source code of the Fort, and find the definition (D.) there you can see that it consists of <# #S #>. These three words were invented by Chuck Moore and lay out the conversion of a number into a string in three stages. The first is the preparation of the transformation, the second is the transformation itself, and the third is the output of the result. Due to the fact that the conversion of the number occurs from the lower digits, and the seal comes from the higher digits - for the conversion you need some temporary buffer where the numbers will be added and where the result will be taken from. Buffer size is taken with a good margin. The word HOLD just removes the next character from the stack to the buffer. HOLDS, a plural form of HOLD, does the same thing with several characters aka a string. A couple of words> R R> is used as an easy implementation of local variables, when you need to postpone some value from the stack for a while.
Such a balancing act allows you not to get additional buffers, not to copy one and the same to a bunch of places, not to bother with libraries to merge lines, but to do everything in the system buffer practically intended for this purpose.

Now look at the server responses. There are only two of them: + OK or -ERR, after which some text or numbers may or may not follow. It would be nice if the server responses somehow did all the work themselves. So that we do not have to jump on the endless branching. And the fort can allow us that.
Imagine a buffer in which the server response is stored as an input stream for interpretation. The word EVALUATE we can interpret an arbitrary string. We use this feature.
Server response always starts with + OK or -ERR. We put the same meaning in these answers.

The easiest with -ERR. In case of an error, it would be nice to output the rest of the line to the terminal and interrupt the execution of the program.

  : -ERR 1 PARSE TYPE CR ABORT; 


PARSE is used to get the tail of an interpreted string.

+ OK is not that simple. It always pops up if the command is processed successfully. Everything tells us well, but what is the context of this good?
The first + OK reports a successful connection to the server. We need to accept this fact and bring the server greeting to the terminal.
The second and third + OK arise in the login procedure. Just ignore the rest of the line.
The fourth + OK appears in response to the STAT and to it, to the + OK, two numbers are attached. The first is the number of messages in the box. The second is the number of parrots occupied by them. We do not need parrots. So we will need to convert the text number from the server response to the number on the stack, and ignore the rest of the line.
The fifth + OK will inform you that the letter we need will follow. There is one nuance in the protocol. First, a pause between the server’s response and the transmission of the letter. Secondly, two numbers following + OK - the message number and its size in parrots.
Sixth + OK, you can not wait, but you can handle as well as the first.
So, + OK should be able to do three different actions.
The fort allows you to do without flags and complex logic. There are so-called vector words in it. Words that can be assigned to action at any desired moment.

 VECT + OK

 : do_type 1 PARSE TYPE CR;
 : do_ignore 1 PARSE 2DROP;
 : do_number 1 PARSE? SLITERAL;
 : do_msg         
                 sockt>                 
                 BEGIN buf bytes_read TYPE CR 
                 bytes_read bufsize - 0 <0 = WHILE sockt> REPEAT do_ignore;



Now we have everything to put the code together.

Code
 ~ nn / lib / sock2.f  

 0 VALUE sockt
 0 VALUE bytes_read
 0 VALUE num_of_msgs

 8192 CONSTANT bufsize
 bufsize ALLOCATE THROW CONSTANT buf
 
 MODULE:

 :> sockt   
               sockt WriteSocketLine THROW;

 : sockt>
               buf bufsize sockt ReadSocket THROW TO bytes_read;

 : response  
                     sockt>
                      buf bytes_read EVALUATE;


 : pop3_noarg 
                      CREATE LATEST, 
                      DOES> @ COUNT> sockt response; 

 : pop3_noargs 
                     0 DO pop3_noarg LOOP;

 4 pop3_noargs NOOP QUIT STAT RSET


 : pop3_digiarg 
                CREATE LATEST, 
                DOES> @> R <# S> D #S BL HOLD R> COUNT HOLDS #>> sockt response;

 : pop3_digiargs 0 DO pop3_digiarg LOOP;

 3 pop3_digiargs DELE LIST RETR


 : user S "USER username"> sockt response;
 : pass S "PASS password"> sockt response;


 VECT + OK

 : do_type 1 PARSE TYPE CR;
 : do_ignore 1 PARSE 2DROP;
 : do_number 1 PARSE? SLITERAL do_ignore;
 : do_msg         
                 sockt>                 
                 BEGIN buf bytes_read TYPE CR 
                 bytes_read bufsize - 0 <0 = WHILE sockt> REPEAT do_ignore;

 : -ERR do_type ABORT;


 : start_sockets   
                        SocketsStartup THROW
                        CreateSocket THROW TO sockt;

 : connect_to_host
                       GetHostIP THROW 
                      110 sockt ConnectSocket THROW 
                       response;

 \ Below is the executable code.  Above - the necessary definitions.

 start_sockets

 'do_type TO + OK 

 S "pop.server.com" connect_to_host 

 'do_ignore TO + OK

 user pass  

 'do_number TO + OK 

 STAT TO num_of_msgs

 'do_msg TO + OK 
 num_of_msgs RETR  

 'do_type TO + OK
 QUIT 


It is easy to add this code so that it downloads all or some number of messages from the server, saves them to a local disk, deletes them from the server, etc. Here I tried to demonstrate the principle of decomposition that is important for programming in the Fort language, developed and described by Leo Brody, and some tools available in the language for solving problems.

FORTH: nano-servers and nanoclients. Part 1

PS: When writing the topic Habr suffered a bit.

Source: https://habr.com/ru/post/210128/


All Articles