/*
libpop3 is a simple POP3 library that allows to connect to a pop3
mailbox, and retrieve/remove messages. This library is using the
RFC1939 as reference, and uses most basic POP3 commands to provide
maximum compatibility across servers.
The library uses following POP3 commands:
USER, PASS, LIST, RETR, DELE, QUIT.
Copyright (C) 2012,2013,2014 Mateusz Viste
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
#include <arpa/inet.h> /* htons() */
#include <netdb.h> /* getaddrinfo() */
#include <stdio.h> /* snprintf() */
#include <stdlib.h> /* free() */
#include <string.h> /* memset() */
#include <sys/select.h> /* select() */
#include <sys/socket.h> /* recv(), send() */
#include <time.h> /* time() */
#include <unistd.h> /* close() */
#include "libpop3.h"
/* The list of defined values below is used for convenience, to avoid human bugs in the code of the library. */
#define ERR_COULDNTCONNECT -1
#define ERR_CONNCLOSEDUNEXP -2
#define ERR_SELECTERR -3
#define ERR_TIMEOUT -4
#define ERR_LINEOVERFLOW -5
#define ERR_BANNERWAIT -6
#define ERR_SRVNOTREADYNEGBANN -7
#define ERR_AUTHREJECT -8
#define ERR_LISTERR -9
#define ERR_MALLOCFAILED -10
#define ERR_LISTCORRUPTED -11
#define ERR_QUIT -12
#define ERR_DELEFAILED -13
#define ERR_RETRERR -14
#define ERR_BUFFTOOSHORT -15
#define ERR_DNSERR -16
/* This is an internal function, hidden to the external world. It's used to read a line of text from the 'sock' socket. Data is stored into the 'buff' buffer, up to maxlen bytes. The function will add a NULL char at the end of the buffer. \r and \n end of line bytes are stripped. The function waits no longer than 'timeout' seconds.
Returns the length of the line on success (can be zero), or a negative value otherwise. The returned error code can be translated into a humanly readable error message via libpop3_strerr(). */
static int libpop3_sockgetline(int sock, char *buff, int maxlen, int timeout) {
struct timeval selecttimeout;
fd_set fdsettable; /* this is a set of file descriptors for select() to poll */
time_t timestart = time(NULL); /* save current time */
int selectresult, bufflen = 0;
for (;;) {
selecttimeout.tv_usec = 0; /* set the select() timeout to block for 1s max */
selecttimeout.tv_sec = 1; /* we do it at every loop, because the timeval becomes undefined after every select() call */
FD_ZERO(&fdsettable); /* clear the whole set of descriptors, we will rebuild it in a moment */
FD_SET(sock, &fdsettable); /* add the socket to read */
selectresult = select(sock+1, &fdsettable, NULL, NULL, &selecttimeout); /* poll socket via select() */
if (selectresult > 0) { /* if there is anything to read... */
char bytebuff;
int recvres = recv(sock, &bytebuff, 1, MSG_WAITALL);
if (recvres <= 0) return(ERR_CONNCLOSEDUNEXP);
if (recvres == 1) {
if (bytebuff != '\r') { /* ignore CR chars */
if (bytebuff == '\n') { /* end of line */
buff[bufflen] = 0;
return(bufflen);
}
buff[bufflen] = bytebuff;
bufflen += 1;
if (bufflen == maxlen) return(ERR_LINEOVERFLOW);
}
}
} else if (selectresult < 0) { /* select() error */
return(ERR_SELECTERR);
}
/* check for timeout */
if ((time(NULL) - timestart) >= timeout) return(ERR_TIMEOUT);
}
}
/* Connect to the host 'host' on port 'port'. 'host' can be a hostname, an IPv4 address or an IPv6 address.
Returns a socket on success, or negative value on failure. */
static int tcpconnect(char *host, int port) {
int sock;
struct addrinfo hints;
struct addrinfo *result, *rp;
char portstr[8];
snprintf(portstr, 8, "%d", port); /* generates a string with port, as required by getaddrinfo */
/* Obtain address(es) matching host/port */
memset(&hints, 0, sizeof(struct addrinfo));
hints.ai_family = AF_UNSPEC; /* Allow IPv4 or IPv6 */
hints.ai_socktype = SOCK_STREAM;
hints.ai_flags = AI_ADDRCONFIG;
hints.ai_protocol = 0; /* Any protocol */
if (getaddrinfo(host, portstr, &hints, &result) != 0) return(ERR_DNSERR);
/* getaddrinfo() returns a list of address structures. Try each address until we successfully connect(2).
If socket(2) (or connect(2)) fails, we (close the socket and) try the next address. */
for (rp = result; rp != NULL; rp = rp->ai_next) {
sock = socket(rp->ai_family, rp->ai_socktype, rp->ai_protocol);
if (sock >= 0) {
if (connect(sock, rp->ai_addr, rp->ai_addrlen) != -1) break; /* Success */
close(sock);
}
}
if (rp == NULL) { /* No address succeeded */
freeaddrinfo(result); /* No longer needed */
return(ERR_COULDNTCONNECT);
}
freeaddrinfo(result); /* No longer needed */
return(sock);
}
/* Deallocaates all memory used by the 'mlist' maillist. This will call free() on every node of the linked list. If 'mlist' is NULL, nothing will be done. */
void libpop3_destroymlist(struct maillist *mlist) {
struct maillist *curmlist = mlist, *nextmlist;
while (curmlist != NULL) {
nextmlist = curmlist->nextmail;
free(curmlist);
curmlist = nextmlist;
}
}
/* Connects to the 'server' POP3 server on port 'port', trying to authenticate using login 'login' and password 'pass'. The 'server' parameter can be a FQDN, or an IPv4/IPv6 IP address. The list of mails will be generated automatically, and the first mail of the linked list will be pointed by *mlist. *mlist will be set to NULL if no mail awaits on the server. Don't forget to call libpop3_destroymlist() on the new maillist when you are done!
Returns a socket to the POP3 connection on success, and a negative number on failure. Failure codes can then be transformed into humanly readable error messages via the libpop3_strerr() function. */
int libpop3_connect(char *server, int port, char *login, char *pass, struct maillist **mlist) {
int sock; /* Socket descriptor */
#define pop3buff_maxlen 1024
char pop3buff[pop3buff_maxlen];
char *mailidstr, *lengthstr;
struct maillist *curlist;
int x, state;
sock = tcpconnect(server, port);
if (sock < 0) return(sock); /* quit on error */
/* wait for the answer and check it */
if (libpop3_sockgetline(sock, pop3buff, pop3buff_maxlen, 30) < 1) {
close(sock);
return(ERR_BANNERWAIT);
} else if (pop3buff[0] != '+') { /* check answer */
close(sock);
return(ERR_SRVNOTREADYNEGBANN);
}
/* send the USER command */
snprintf(pop3buff, pop3buff_maxlen, "USER %s\r\n", login);
if (send(sock, pop3buff, strlen(pop3buff), MSG_WAITALL) != (signed) strlen(pop3buff)) {
close(sock);
return(ERR_CONNCLOSEDUNEXP);
}
/* wait for the answer and check it */
if (libpop3_sockgetline(sock, pop3buff, pop3buff_maxlen, 30) < 1) {
close(sock);
return(ERR_CONNCLOSEDUNEXP);
} else if (pop3buff[0] != '+') { /* check answer */
close(sock);
return(ERR_AUTHREJECT);
}
/* send the PASS command */
snprintf(pop3buff, pop3buff_maxlen, "PASS %s\r\n", pass);
if (send(sock, pop3buff, strlen(pop3buff), MSG_WAITALL) != (signed) strlen(pop3buff)) {
close(sock);
return(ERR_CONNCLOSEDUNEXP);
}
/* wait for the answer and check it */
if (libpop3_sockgetline(sock, pop3buff, pop3buff_maxlen, 30) < 1) {
close(sock);
return(ERR_CONNCLOSEDUNEXP);
} else if (pop3buff[0] != '+') { /* check answer */
close(sock);
return(ERR_AUTHREJECT);
}
/* send the LIST command */
snprintf(pop3buff, pop3buff_maxlen, "LIST\r\n");
if (send(sock, pop3buff, strlen(pop3buff), MSG_WAITALL) != (signed) strlen(pop3buff)) {
close(sock);
return(ERR_CONNCLOSEDUNEXP);
}
/* wait for the answer and check it */
if (libpop3_sockgetline(sock, pop3buff, pop3buff_maxlen, 10) < 1) {
close(sock);
return(ERR_CONNCLOSEDUNEXP);
} else if (pop3buff[0] != '+') { /* check answer */
close(sock);
return(ERR_LISTERR);
}
*mlist = NULL;
curlist = *mlist;
/* wait for the multiline answer */
for (;;) {
if (libpop3_sockgetline(sock, pop3buff, pop3buff_maxlen, 10) < 1) {
close(sock);
return(ERR_CONNCLOSEDUNEXP);
}
if (pop3buff[0] == '.') break; /* end of list */
if (curlist == NULL) { /* this is the first mail */
*mlist = malloc(sizeof(struct maillist));
curlist = *mlist;
} else { /* this is some other mail */
curlist->nextmail = malloc(sizeof(struct maillist));
curlist = curlist->nextmail;
}
if (curlist == NULL) { /* memory error */
libpop3_destroymlist(*mlist); /* free all memory that we took so far */
*mlist = NULL; /* set mlist to NULL to indicate that there is nothing there */
return(ERR_MALLOCFAILED);
}
curlist->nextmail = NULL; /* write the linked list terminator */
/* process the answer line to retrieve the mail id and size */
mailidstr = pop3buff;
lengthstr = NULL;
state = 0;
for (x = 0; pop3buff[x] != 0; x++) {
if (pop3buff[x] == ' ') {
pop3buff[x] = 0; /* replace spaces by string terminators */
if (state == 0) state = 1; /* if was reading mailid, switch to 'looking for size' */
} else { /* character is not a space */
if (state == 1) { /* looking for size? found it! */
state = 2;
lengthstr = &pop3buff[x];
}
}
}
if (lengthstr == NULL) { /* corrupted LIST result */
libpop3_destroymlist(*mlist);
*mlist = NULL;
return(ERR_LISTCORRUPTED);
}
/* write values into the curlist structure and reloop */
curlist->mailid = atoi(mailidstr);
curlist->length = atoi(lengthstr);
}
return(sock); /* return the socket to the POP3 connection */
}
/* Gets the content of the mail with id 'mailid' from server pointed by 'popconn'. The content of the mail is written into the memory space pointed by 'mailmsg', up to 'maxlen' bytes. the mail will be terminated with a NULL byte as a string terminator. Also, the function needs a 4 additional bytes of memory for its own needs, therefore make sure to allocate mailmsg with at least 5 bytes more than the actual expected length of the message.
Returns the length of the fetched message on success, a negative value otherwise. The returned error code can be translated into an error message via libpop3_strerr(). */
int libpop3_getmail(int popconn, int mailid, char *mailmsg, int maxlen) {
char buff[256];
int msglen = 0, state = 0, lastlf = 0;
snprintf(buff, 256, "RETR %d\r\n", mailid);
if (send(popconn, buff, strlen(buff), MSG_WAITALL) != (signed) strlen(buff)) {
close(popconn);
return(ERR_CONNCLOSEDUNEXP);
}
/* wait for the answer and check it */
if (libpop3_sockgetline(popconn, buff, 256, 20) < 1) {
close(popconn);
return(ERR_CONNCLOSEDUNEXP);
} else if (buff[0] != '+') { /* check answer */
return(ERR_RETRERR);
}
/* start processing the mail, and store it into mailmsg */
for (;;) {
if (recv(popconn, buff, 1, MSG_WAITALL) != 1) return(ERR_CONNCLOSEDUNEXP);
if (buff[0] != '\r') {
if (buff[0] == '\n') {
if (state == 2) { /* end of message */
msglen = lastlf + 1; /* trim out the dot terminator */
break;
} else {
lastlf = msglen;
state = 1;
}
} else { /* not a LF */
if (buff[0] == '.') {
if (state == 1) {
state = 2;
} else if (state == 2) {
msglen -= 1; /* ignore dot stuffing */
state = 0;
}
} else {
state = 0;
}
}
}
mailmsg[msglen++] = buff[0];
if (msglen >= maxlen) return(ERR_BUFFTOOSHORT);
}
mailmsg[msglen] = 0;
return(msglen);
}
/* Removes the mail with id 'mailid' from the pop3 connection pointed by 'popconn'. Returns 0 on success, non-zero otherwise. The returned error code can be translated into an error message via libpop3_strerr(). */
int libpop3_delmail(int popconn, int mailid) {
char buff[1024];
snprintf(buff, 1024, "DELE %d\r\n", mailid);
if (send(popconn, buff, strlen(buff), MSG_WAITALL) != (signed) strlen(buff)) {
close(popconn);
return(ERR_CONNCLOSEDUNEXP);
}
/* wait for the answer and check it */
if (libpop3_sockgetline(popconn, buff, 1024, 30) < 1) {
close(popconn);
return(ERR_CONNCLOSEDUNEXP);
} else if (buff[0] != '+') { /* check answer */
return(ERR_DELEFAILED);
}
return(0);
}
/* Closes the communication with the pop3 server pointed by 'popconn'. Returns 0 on success, non-zero otherwise. The returned error code can be translated into an error message via libpop3_strerr(). */
int libpop3_disconnect(int popconn) {
char buff[1024];
if (send(popconn, "QUIT\r\n", 6, MSG_WAITALL) != 6) {
close(popconn);
return(ERR_CONNCLOSEDUNEXP);
}
/* wait for the answer and check it */
if (libpop3_sockgetline(popconn, buff, 1024, 30) < 1) {
close(popconn);
return(ERR_CONNCLOSEDUNEXP);
} else if (buff[0] != '+') { /* check answer */
close(popconn);
return(ERR_QUIT);
}
close(popconn);
return(0);
}
/* Translates a libpop3 error code into a humanly readable error message. Takes the libpop3 error code as parameter, and returns a pointer to the error message string. */
char *libpop3_strerr(int errcode) {
if (errcode >= 0) return("Success");
switch (errcode) {
case ERR_COULDNTCONNECT:
return("Could not establish a TCP connection with the remote server");
break;
case ERR_CONNCLOSEDUNEXP:
return("Connection with the server has been unexpectedly closed");
break;
case ERR_SELECTERR:
return("Unexpected select() error");
break;
case ERR_TIMEOUT:
return("Timeout");
break;
case ERR_LINEOVERFLOW:
return("Line buffer overflow");
break;
case ERR_BANNERWAIT:
return("Connection error while waiting for server banner");
break;
case ERR_SRVNOTREADYNEGBANN:
return("POP3 server not ready (negative banner)");
break;
case ERR_AUTHREJECT:
return("POP3 auth failed");
break;
case ERR_LISTERR:
return("Server rejected our LIST request");
break;
case ERR_MALLOCFAILED:
return("Not enough memory");
break;
case ERR_LISTCORRUPTED:
return("Server sent a corrupted answer to LIST");
break;
case ERR_QUIT:
return("Server returned an error condition before QUITing");
break;
case ERR_DELEFAILED:
return("DELE operation failed on server");
break;
case ERR_RETRERR:
return("RETR operation failed on server");
break;
case ERR_BUFFTOOSHORT:
return("Supplied memory buffer is too short");
break;
case ERR_DNSERR:
return("Name resolution failure");
break;
default:
return("Unknown error");
break;
}
}