[balsa] Move POP3 support to the GIO-based networking lib



commit 08fc014bf7fc57a169f93feb6ad884ea5d7dad55
Author: Peter Bloomfield <PeterBloomfield bellsouth net>
Date:   Fri Apr 7 19:46:29 2017 -0400

    Move POP3 support to the GIO-based networking lib
    
    This part of the patch adds the POP3 implementation and unit tests to libnetclient, and adds a few small 
improvements to the existing files.
    
    * libnetclient/net-client-pop.c, libnetclient/net-client-pop.h: implement the NetClientPop class
    * libnetclient/Makefile.am: include net-client-pop.[hc] in the build
    * libnetclient/README: update documentation
    * libnetclient/net-client-smtp.c: as the starttls and authentication capabilities are required in the 
connect method only, remove them from the object data; improve error checking
    * libnetclient/net-client-smtp.h, libnetclient/net-client.h: documentation fixes
    * libnetclient/net-client.c: add more debug messages
    * libnetclient/test/Makefile.am, libnetclient/test/inetsim.conf, libnetclient/test/tests.c: include 
NetClientPop unit tests
    * libnetclient/test/inetsim-1.2.6-POP3.diff: patch for INetSim fixing two bugs in its POP3 simulation, 
and adding pipelining support (see libnetclient/README)
    * libnetclient/test/start-test-env.sh: generated file, should be removed from the repo

 libnetclient/Makefile.am                  |    2 +
 libnetclient/README                       |    7 +-
 libnetclient/net-client-pop.c             |  808 +++++++++++++++++++++++++++++
 libnetclient/net-client-pop.h             |  262 ++++++++++
 libnetclient/net-client-smtp.c            |   55 +-
 libnetclient/net-client-smtp.h            |    4 +-
 libnetclient/net-client.c                 |   13 +-
 libnetclient/net-client.h                 |   12 +-
 libnetclient/test/Makefile.am             |    5 +-
 libnetclient/test/inetsim-1.2.6-POP3.diff |   45 ++
 libnetclient/test/inetsim.conf            |    7 +-
 libnetclient/test/start-test-env.sh       |   20 -
 libnetclient/test/tests.c                 |  251 +++++++++-
 13 files changed, 1413 insertions(+), 78 deletions(-)
---
diff --git a/libnetclient/Makefile.am b/libnetclient/Makefile.am
index 3562e41..89da9de 100644
--- a/libnetclient/Makefile.am
+++ b/libnetclient/Makefile.am
@@ -4,6 +4,8 @@ noinst_LIBRARIES = libnetclient.a
 libnetclient_a_SOURCES =       \
        net-client.c                    \
        net-client.h                    \
+       net-client-pop.c                \
+       net-client-pop.h                \
        net-client-smtp.c               \
        net-client-smtp.h               \
        net-client-utils.c              \
diff --git a/libnetclient/README b/libnetclient/README
index f5d4d23..cdebc57 100644
--- a/libnetclient/README
+++ b/libnetclient/README
@@ -19,7 +19,8 @@ Purpose
 
 This library provides an implementation of CRLF-terminated line-based client
 protocols built on top of GIO.  It provides a base module, containing the
-line-based IO methods, and on top of that a SMTP (RFC5321) client class.
+line-based IO methods, and on top of that POP3 (RFC1939) and SMTP (RFC5321)
+client classes.
 
 
 Coding Style
@@ -72,6 +73,10 @@ are required for running them:
 Note that most of these requirements are typically available pre-packaged
 for your favorite distribution.
 
+Unfortunately, INetSim 1.2.6 has two little bugs in POP3 handling and does
+not offer pipelining.  The file test/inetsim-1.2.6-POP3.diff contains a
+patch which fixes these issues.
+
 For running the tests, open two terminal windows, and cd to the test folder
 of this package.
 
diff --git a/libnetclient/net-client-pop.c b/libnetclient/net-client-pop.c
new file mode 100644
index 0000000..7145276
--- /dev/null
+++ b/libnetclient/net-client-pop.c
@@ -0,0 +1,808 @@
+/* NetClient - simple line-based network client library
+ *
+ * Copyright (C) Albrecht Dreß <mailto:albrecht dress arcor de> 2017
+ *
+ * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser 
General Public License
+ * as published by the Free Software Foundation; either version 3 of the License, or (at your option) any 
later version.
+ *
+ * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even 
the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more 
details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along with this library. If not, 
see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <glib/gi18n.h>
+#include "net-client-utils.h"
+#include "net-client-pop.h"
+
+
+struct _NetClientPopPrivate {
+       NetClientCryptMode crypt_mode;
+       gchar *apop_banner;
+       guint auth_allowed[2];                  /** 0: encrypted, 1: unencrypted */
+       gboolean can_pipelining;
+       gboolean can_uidl;
+       gboolean use_pipelining;
+};
+
+
+/* Note: the maximum line length of a message body downloaded from the POP3 server may be up to 998 chars, 
excluding the terminating
+ * CRLF, see RFC 5322, Sect. 2.1.1.  However, it also states that "Receiving implementations would do well 
to handle an arbitrarily
+ * large number of characters in a line for robustness sake", so we actually accept lines from POP3 of 
unlimited length. */
+#define MAX_POP_LINE_LEN                       0U
+#define POP_DATA_BUF_SIZE                      4096U
+
+
+/*lint -save -e9026            allow function-like macros, see MISRA C:2012, Directive 4.9 */
+#define IS_ML_TERM(str)                                ((str[0] == '.') && (str[1] == '\0'))
+/*lint -emacro(9079,POP_MSG_INFO) -emacro(9087,POP_MSG_INFO)
+ * allow conversion of GList data pointer, MISRA C:2012, Rules 11.3, 11.5 */
+#define POP_MSG_INFO(list)                             ((NetClientPopMessageInfo *) ((list)->data))
+/*lint -restore */
+
+
+G_DEFINE_TYPE(NetClientPop, net_client_pop, NET_CLIENT_TYPE)
+
+
+static void net_client_pop_finalise(GObject *object);
+static void net_client_pop_get_capa(NetClientPop *client, guint *auth_supported);
+static gboolean net_client_pop_read_reply(NetClientPop *client, gchar **reply, GError **error);
+static gboolean net_client_pop_uidl(NetClientPop *client, GList * const *msg_list, GError **error);
+static gboolean net_client_pop_starttls(NetClientPop *client, GError **error);
+static gboolean net_client_pop_execute(NetClientPop *client, const gchar *request_fmt, gchar **last_reply, 
GError **error, ...)
+       G_GNUC_PRINTF(2, 5);
+static gboolean net_client_pop_execute_sasl(NetClientPop *client, const gchar *request_fmt, gchar 
**challenge, GError **error, ...);
+static gboolean net_client_pop_auth(NetClientPop *client, const gchar *user, const gchar *passwd, guint 
auth_supported,
+                                                                       GError **error);
+static gboolean net_client_pop_auth_plain(NetClientPop *client, const gchar* user, const gchar* passwd, 
GError** error);
+static gboolean net_client_pop_auth_login(NetClientPop *client, const gchar *user, const gchar *passwd, 
GError **error);
+static gboolean net_client_pop_auth_user_pass(NetClientPop *client, const gchar* user, const gchar* passwd, 
GError** error);
+static gboolean net_client_pop_auth_apop(NetClientPop *client, const gchar* user, const gchar* passwd, 
GError** error);
+static gboolean net_client_pop_auth_cram(NetClientPop *client, GChecksumType chksum_type, const gchar *user, 
const gchar *passwd,
+                                                                                 GError **error);
+static gboolean net_client_pop_retr_msg(NetClientPop *client, const NetClientPopMessageInfo *info, 
NetClientPopMsgCb callback,
+                                                                               gpointer user_data, GError 
**error);
+
+
+NetClientPop *
+net_client_pop_new(const gchar *host, guint16 port, NetClientCryptMode crypt_mode, gboolean use_pipelining)
+{
+       NetClientPop *client;
+
+       g_return_val_if_fail((host != NULL) && (crypt_mode >= NET_CLIENT_CRYPT_ENCRYPTED) && (crypt_mode <= 
NET_CLIENT_CRYPT_NONE),
+               NULL);
+
+       client = NET_CLIENT_POP(g_object_new(NET_CLIENT_POP_TYPE, NULL));
+       if (client != NULL) {
+               if (!net_client_configure(NET_CLIENT(client), host, port, MAX_POP_LINE_LEN, NULL)) {
+                       g_object_unref(G_OBJECT(client));
+                       client = NULL;
+               } else {
+                       client->priv->crypt_mode = crypt_mode;
+                       client->priv->use_pipelining = use_pipelining;
+               }
+       }
+
+       return client;
+}
+
+
+gboolean
+net_client_pop_allow_auth(NetClientPop *client, gboolean encrypted, guint allow_auth)
+{
+       /* paranoia check */
+       g_return_val_if_fail(NET_IS_CLIENT_POP(client), FALSE);
+       if (encrypted) {
+               client->priv->auth_allowed[0] = allow_auth;
+       } else {
+               client->priv->auth_allowed[1] = allow_auth;
+       }
+       return TRUE;
+}
+
+
+gboolean
+net_client_pop_connect(NetClientPop *client, gchar **greeting, GError **error)
+{
+       gchar *server_msg = NULL;
+       guint auth_supported = 0U;
+       gboolean result;
+
+       /* paranoia checks */
+       g_return_val_if_fail(NET_IS_CLIENT_POP(client), FALSE);
+
+       /* establish connection, and immediately switch to TLS if required */
+       result = net_client_connect(NET_CLIENT(client), error);
+       if (result && (client->priv->crypt_mode == NET_CLIENT_CRYPT_ENCRYPTED)) {
+               result = net_client_start_tls(NET_CLIENT(client), error);
+       }
+
+       /* get the greeting */
+       if (result) {
+               result = net_client_pop_read_reply(client, &server_msg, error);
+       }
+
+       /* extract the APOP banner */
+       if (result) {
+               const gchar *ang_open;
+
+               ang_open = strchr(server_msg, '<');             /*lint !e668 !e9034             server_msg 
cannot be NULL; accept char literal as int */
+               if (ang_open != NULL) {
+                       const gchar *ang_close;
+
+                       ang_close = strchr(ang_open, '>');      /*lint !e9034   accept char literal as int */
+                       if (ang_close != NULL) {
+                               /*lint -e{946,947}      allowed exception according to MISRA Rules 18.2 and 
18.3 */
+                               client->priv->apop_banner = g_strndup(ang_open, (ang_close - ang_open) + 1);
+                               auth_supported = NET_CLIENT_POP_AUTH_APOP;
+                       }
+               }
+               if (greeting != NULL) {
+                       *greeting = g_strdup(server_msg);
+               }
+               g_free(server_msg);
+       }
+
+       /* perform STLS if required- note that some servers support STLS, but do not announce it.  So just 
try... */
+       if (result &&
+               ((client->priv->crypt_mode == NET_CLIENT_CRYPT_STARTTLS) || (client->priv->crypt_mode == 
NET_CLIENT_CRYPT_STARTTLS_OPT))) {
+               result = net_client_pop_starttls(client, error);
+               if (!result) {
+                       if (client->priv->crypt_mode == NET_CLIENT_CRYPT_STARTTLS_OPT) {
+                               result = TRUE;
+                               g_clear_error(error);
+                       }
+               }
+       }
+
+       /* read the capabilities (which may be unsupported, so ignore any negative result) */
+       if (result) {
+               net_client_pop_get_capa(client, &auth_supported);
+       }
+
+       /* authenticate if we were successful so far */
+       if (result) {
+               gchar **auth_data;
+
+               auth_data = NULL;
+               g_debug("emit 'auth' signal for client %p", client);
+               g_signal_emit_by_name(client, "auth", &auth_data);
+               if ((auth_data != NULL) && (auth_data[0] != NULL) && (auth_data[1] != NULL)) {
+                       result = net_client_pop_auth(client, auth_data[0], auth_data[1], auth_supported, 
error);
+                       memset(auth_data[0], 0, strlen(auth_data[0]));
+                       memset(auth_data[1], 0, strlen(auth_data[1]));
+               }
+               g_strfreev(auth_data);
+       }
+
+       return result;
+}
+
+
+gboolean
+net_client_pop_stat(NetClientPop *client, gsize *msg_count, gsize *mbox_size, GError **error)
+{
+       gboolean result;
+       gchar *stat_buf;
+
+       /* paranoia checks */
+       g_return_val_if_fail(NET_IS_CLIENT_POP(client), FALSE);
+
+       /* run the STAT command */
+       result = net_client_pop_execute(client, "STAT", &stat_buf, error);
+       if (result) {
+               unsigned long count;
+               unsigned long total_size;
+
+               if (sscanf(stat_buf, "%lu %lu", &count, &total_size) == 2) {
+                       if (msg_count != NULL) {
+                               *msg_count = count;
+                       }
+                       if (mbox_size != NULL) {
+                               *mbox_size = total_size;
+                       }
+               } else {
+                       result = FALSE;
+                       g_set_error(error, NET_CLIENT_POP_ERROR_QUARK, (gint) NET_CLIENT_ERROR_POP_PROTOCOL, 
_("bad server reply: %s"),
+                               stat_buf);
+               }
+
+               g_free(stat_buf);
+       }
+
+       return result;
+}
+
+
+gboolean
+net_client_pop_list(NetClientPop *client, GList **msg_list, gboolean with_uid, GError **error)
+{
+       gboolean result;
+       gboolean done;
+
+       /* paranoia checks */
+       g_return_val_if_fail(NET_IS_CLIENT_POP(client) && (msg_list != NULL), FALSE);
+
+       *msg_list = NULL;
+
+       /* run the LIST command */
+       result = net_client_pop_execute(client, "LIST", NULL, error);
+       done = FALSE;
+       while (result && !done) {
+               gchar *reply;
+
+               result = net_client_read_line(NET_CLIENT(client), &reply, error);
+               if (result) {
+                       if (IS_ML_TERM(reply)) {
+                               done = TRUE;
+                       } else {
+                               NetClientPopMessageInfo *info;
+
+                               info = g_new0(NetClientPopMessageInfo, 1U);
+                               *msg_list = g_list_prepend(*msg_list, info);
+                               if (sscanf(reply, "%u %lu", &info->id, &info->size) != 2) {
+                                       result = FALSE;
+                                       g_set_error(error, NET_CLIENT_POP_ERROR_QUARK, (gint) 
NET_CLIENT_ERROR_POP_PROTOCOL, _("bad server reply"));
+                               }
+                       }
+                       g_free(reply);
+               }
+       }
+
+       /* on success, turn the list into the order reported by the remote server */
+       if (result) {
+               *msg_list = g_list_reverse(*msg_list);
+       }
+
+       /* get all uid's if requested */
+       if (result && with_uid && client->priv->can_uidl && (*msg_list != NULL)) {
+               result = net_client_pop_uidl(client, msg_list, error);
+       }
+
+       if (!result) {
+               g_list_free_full(*msg_list, (GDestroyNotify) net_client_pop_msg_info_free);
+       }
+
+       return result;
+}
+
+
+gboolean
+net_client_pop_retr(NetClientPop *client, GList *msg_list, NetClientPopMsgCb callback, gpointer user_data, 
GError **error)
+{
+       gboolean result;
+       gboolean pipelining;
+       const GList *p;
+
+       /* paranoia checks */
+       g_return_val_if_fail(NET_IS_CLIENT_POP(client) && (msg_list != NULL) && (callback != NULL), FALSE);
+
+       /* pipelining: send all RETR commands */
+       pipelining = client->priv->can_pipelining && client->priv->use_pipelining;
+       if (pipelining) {
+               GString *retr_buf;
+
+               retr_buf = g_string_sized_new(10U * g_list_length(msg_list));
+               for (p = msg_list; p != NULL; p = p->next) {
+                       g_string_append_printf(retr_buf, "RETR %u\r\n", POP_MSG_INFO(p)->id);
+               }
+               result = net_client_write_buffer(NET_CLIENT(client), retr_buf->str, retr_buf->len, error);
+               (void) g_string_free(retr_buf, TRUE);
+       } else {
+               result = TRUE;
+       }
+
+       for (p = msg_list; result && (p != NULL); p = p->next) {
+               const NetClientPopMessageInfo *info = POP_MSG_INFO(p);
+
+               if (pipelining) {
+                       result = net_client_pop_read_reply(client, NULL, error);
+               } else {
+                       result = net_client_pop_execute(client, "RETR %u", NULL, error, info->id);
+               }
+               if (result) {
+                       result = net_client_pop_retr_msg(client, info, callback, user_data, error);
+               }
+       }
+
+       return result;
+}
+
+
+gboolean
+net_client_pop_dele(NetClientPop *client, GList *msg_list, GError **error)
+{
+       gboolean result;
+       gboolean pipelining;
+       const GList *p;
+
+       /* paranoia checks */
+       g_return_val_if_fail(NET_IS_CLIENT_POP(client) && (msg_list != NULL), FALSE);
+
+       /* pipelining: send all DELE commands */
+       pipelining = client->priv->can_pipelining && client->priv->use_pipelining;
+       if (pipelining) {
+               GString *dele_buf;
+
+               dele_buf = g_string_sized_new(10U * g_list_length(msg_list));
+               for (p = msg_list; p != NULL; p = p->next) {
+                       g_string_append_printf(dele_buf, "DELE %u\r\n", POP_MSG_INFO(p)->id);
+               }
+               result = net_client_write_buffer(NET_CLIENT(client), dele_buf->str, dele_buf->len, error);
+               (void) g_string_free(dele_buf, TRUE);
+       } else {
+               result = TRUE;
+       }
+
+       for (p = msg_list; result && (p != NULL); p = p->next) {
+               const NetClientPopMessageInfo *info = POP_MSG_INFO(p);
+
+               if (pipelining) {
+                       result = net_client_pop_read_reply(client, NULL, error);
+               } else {
+                       result = net_client_pop_execute(client, "DELE %u", NULL, error, info->id);
+               }
+       }
+
+       return result;
+
+}
+
+
+void
+net_client_pop_msg_info_free(NetClientPopMessageInfo *info)
+{
+       if (info != NULL) {
+               g_free(info->uid);
+               g_free(info);
+       }
+}
+
+
+/* == local functions 
=========================================================================================================== */
+
+static void
+net_client_pop_class_init(NetClientPopClass *klass)
+{
+       GObjectClass *gobject_class = G_OBJECT_CLASS(klass);
+
+       gobject_class->finalize = net_client_pop_finalise;
+}
+
+
+static void
+net_client_pop_init(NetClientPop *self)
+{
+       self->priv = g_new0(NetClientPopPrivate, 1U);
+       self->priv->auth_allowed[0] = NET_CLIENT_POP_AUTH_ALL;
+       self->priv->auth_allowed[1] = NET_CLIENT_POP_AUTH_SAFE;
+}
+
+
+static void
+net_client_pop_finalise(GObject *object)
+{
+       const NetClientPop *client = NET_CLIENT_POP(object);
+       const GObjectClass *parent_class = G_OBJECT_CLASS(net_client_pop_parent_class);
+
+       /* send the 'QUIT' command - no need to evaluate the reply or check for errors */
+       (void) net_client_execute(NET_CLIENT(client), NULL, "QUIT", NULL);
+
+       if (client->priv != NULL) {
+               g_free(client->priv->apop_banner);
+               g_free(client->priv);
+       }
+       (*parent_class->finalize)(object);
+}
+
+
+/* Note: if supplied, reply is never NULL on success */
+static gboolean
+net_client_pop_read_reply(NetClientPop *client, gchar **reply, GError **error)
+{
+       gboolean result;
+       gchar *reply_buf;
+
+       result = net_client_read_line(NET_CLIENT(client), &reply_buf, error);
+       if (result) {
+               if (strncmp(reply_buf, "+OK", 3U) == 0) {
+                       if ((strlen(reply_buf) > 3U) && (reply != NULL)) {
+                               *reply = g_strdup(&reply_buf[4]);
+                       }
+               } else if (strncmp(reply_buf, "-ERR", 4U) == 0) {
+                       if (strlen(reply_buf) > 4U) {
+                               g_set_error(error, NET_CLIENT_POP_ERROR_QUARK, (gint) 
NET_CLIENT_ERROR_POP_SERVER_ERR, _("error: %s"),
+                                       &reply_buf[5]);
+                       } else {
+                               g_set_error(error, NET_CLIENT_POP_ERROR_QUARK, (gint) 
NET_CLIENT_ERROR_POP_SERVER_ERR, _("error"));
+                       }
+                       result = FALSE;
+               } else {
+                       /* unexpected server reply */
+                       g_set_error(error, NET_CLIENT_POP_ERROR_QUARK, (gint) NET_CLIENT_ERROR_POP_PROTOCOL, 
_("bad server reply: %s"),
+                               reply_buf);
+                       result = FALSE;
+               }
+
+               g_free(reply_buf);
+       }
+
+       return result;
+}
+
+
+/* note: if supplied, last_reply is never NULL on success */
+static gboolean
+net_client_pop_execute(NetClientPop *client, const gchar *request_fmt, gchar **last_reply, GError **error, 
...)
+{
+       va_list args;
+       gboolean result;
+
+       va_start(args, error);          /*lint !e413    a NULL error argument is irrelevant here */
+       result = net_client_vwrite_line(NET_CLIENT(client), request_fmt, args, error);
+       va_end(args);
+
+       if (result) {
+               result = net_client_pop_read_reply(client, last_reply, error);
+       }
+
+       return result;
+}
+
+
+static gboolean
+net_client_pop_starttls(NetClientPop *client, GError **error)
+{
+       gboolean result;
+
+       result = net_client_pop_execute(client, "STLS", NULL, error);
+       if (result) {
+               result = net_client_start_tls(NET_CLIENT(client), error);
+       }
+
+       return result;
+}
+
+
+static gboolean
+net_client_pop_auth(NetClientPop *client, const gchar *user, const gchar *passwd, guint auth_supported, 
GError **error)
+{
+       gboolean result;
+       guint auth_mask;
+
+       g_return_val_if_fail(NET_IS_CLIENT_POP(client) && (user != NULL) && (passwd != NULL), FALSE);
+
+       if (net_client_is_encrypted(NET_CLIENT(client))) {
+               auth_mask = client->priv->auth_allowed[0] & auth_supported;
+       } else {
+               auth_mask = client->priv->auth_allowed[1] & auth_supported;
+       }
+
+       if ((auth_mask & NET_CLIENT_POP_AUTH_CRAM_SHA1) != 0U) {
+               result = net_client_pop_auth_cram(client, G_CHECKSUM_SHA1, user, passwd, error);
+       } else if ((auth_mask & NET_CLIENT_POP_AUTH_CRAM_MD5) != 0U) {
+               result = net_client_pop_auth_cram(client, G_CHECKSUM_MD5, user, passwd, error);
+       } else if ((auth_mask & NET_CLIENT_POP_AUTH_APOP) != 0U) {
+               result = net_client_pop_auth_apop(client, user, passwd, error);
+       } else if ((auth_mask & NET_CLIENT_POP_AUTH_PLAIN) != 0U) {
+               result = net_client_pop_auth_plain(client, user, passwd, error);
+       } else if ((auth_mask & NET_CLIENT_POP_AUTH_USER_PASS) != 0U) {
+               result = net_client_pop_auth_user_pass(client, user, passwd, error);
+       } else if ((auth_mask & NET_CLIENT_POP_AUTH_LOGIN) != 0U) {
+               result = net_client_pop_auth_login(client, user, passwd, error);
+       } else {
+               g_set_error(error, NET_CLIENT_POP_ERROR_QUARK, (gint) NET_CLIENT_ERROR_POP_NO_AUTH,
+                       _("no suitable authentication mechanism"));
+               result = FALSE;
+       }
+
+       return result;
+}
+
+
+static gboolean
+net_client_pop_auth_plain(NetClientPop *client, const gchar *user, const gchar *passwd, GError **error)
+{
+       gboolean result ;
+       gchar *base64_buf;
+
+       base64_buf = net_client_auth_plain_calc(user, passwd);
+       if (base64_buf != NULL) {
+               result = net_client_pop_execute_sasl(client, "AUTH PLAIN", NULL, error);
+               if (result) {
+                       result = net_client_pop_execute(client, "%s", NULL, error, base64_buf);
+               }
+               memset(base64_buf, 0, strlen(base64_buf));
+               g_free(base64_buf);
+       } else {
+               result = FALSE;
+       }
+
+       return result;
+}
+
+
+static gboolean
+net_client_pop_auth_login(NetClientPop *client, const gchar *user, const gchar *passwd, GError **error)
+{
+       gboolean result;
+
+       result = net_client_pop_execute_sasl(client, "AUTH LOGIN", NULL, error);
+       if (result) {
+               gchar *base64_buf;
+
+               base64_buf = g_base64_encode((const guchar *) user, strlen(user));
+               result = net_client_pop_execute_sasl(client, "%s", NULL, error, base64_buf);
+               memset(base64_buf, 0, strlen(base64_buf));
+               g_free(base64_buf);
+               if (result) {
+                       base64_buf = g_base64_encode((const guchar *) passwd, strlen(passwd));
+                       result = net_client_pop_execute(client, "%s", NULL, error, base64_buf);
+                       memset(base64_buf, 0, strlen(base64_buf));
+                       g_free(base64_buf);
+               }
+       }
+
+       return result;
+}
+
+
+static gboolean
+net_client_pop_auth_user_pass(NetClientPop *client, const gchar *user, const gchar *passwd, GError **error)
+{
+       gboolean result;
+
+       result = net_client_pop_execute(client, "USER %s", NULL, error, user);
+       if (result) {
+               result = net_client_pop_execute(client, "PASS %s", NULL, error, passwd);
+       }
+
+       return result;
+}
+
+
+static gboolean
+net_client_pop_auth_apop(NetClientPop *client, const gchar *user, const gchar *passwd, GError **error)
+{
+       gboolean result;
+       gchar *auth_buf;
+       gchar *md5_buf;
+
+       auth_buf = g_strconcat(client->priv->apop_banner, passwd, NULL);
+       md5_buf = g_compute_checksum_for_string(G_CHECKSUM_MD5, auth_buf, -1);
+       memset(auth_buf, 0, strlen(auth_buf));
+       g_free(auth_buf);
+       result = net_client_pop_execute(client, "APOP %s %s", NULL, error, user, md5_buf);
+       memset(md5_buf, 0, strlen(md5_buf));
+       g_free(md5_buf);
+
+       return result;
+}
+
+
+static gboolean
+net_client_pop_auth_cram(NetClientPop *client, GChecksumType chksum_type, const gchar *user, const gchar 
*passwd, GError **error)
+{
+       gboolean result;
+       gchar *challenge = NULL;
+
+       result = net_client_pop_execute_sasl(client, "AUTH CRAM-%s", &challenge, error, 
net_client_chksum_to_str(chksum_type));
+       if (result) {
+               gchar *auth_buf;
+               auth_buf = net_client_cram_calc(challenge, chksum_type, user, passwd);
+               if (auth_buf != NULL) {
+                       result = net_client_pop_execute(client, "%s", NULL, error, auth_buf);
+                       memset(auth_buf, 0, strlen(auth_buf));
+                       g_free(auth_buf);
+               } else {
+                       result = FALSE;
+               }
+       }
+       g_free(challenge);
+
+       return result;
+}
+
+
+/* Note: if supplied, challenge is never NULL on success */
+static gboolean
+net_client_pop_execute_sasl(NetClientPop *client, const gchar *request_fmt, gchar **challenge, GError 
**error, ...)
+{
+       va_list args;
+       gboolean result;
+
+       va_start(args, error);          /*lint !e413    a NULL error argument is irrelevant here */
+       result = net_client_vwrite_line(NET_CLIENT(client), request_fmt, args, error);
+       va_end(args);
+
+       if (result) {
+               gchar *reply_buf;
+
+               result = net_client_read_line(NET_CLIENT(client), &reply_buf, error);
+               if (result) {
+                       if (strncmp(reply_buf, "+ ", 2U) == 0) {
+                               if (challenge != NULL) {
+                                       *challenge = g_strdup(&reply_buf[2]);
+                               }
+                       } else {
+                               result = FALSE;
+                               g_set_error(error, NET_CLIENT_POP_ERROR_QUARK, (gint) 
NET_CLIENT_ERROR_POP_SERVER_ERR, _("error: %s"), reply_buf);
+                       }
+                       g_free(reply_buf);
+               }
+       }
+
+       return result;
+}
+
+
+static void
+net_client_pop_get_capa(NetClientPop *client, guint *auth_supported)
+{
+       gboolean result;
+       gboolean done;
+
+       /* clear all capability flags except APOP and send the CAPA command */
+       *auth_supported = *auth_supported & NET_CLIENT_POP_AUTH_APOP;
+       client->priv->can_pipelining = FALSE;
+       result = net_client_pop_execute(client, "CAPA", NULL, NULL);
+
+       /* evaluate the response */
+       done = FALSE;
+       while (result && !done) {
+               gchar *reply;
+
+               result = net_client_read_line(NET_CLIENT(client), &reply, NULL);
+               if (result) {
+                       if (IS_ML_TERM(reply)) {
+                               done = TRUE;
+                       } else if (strcmp(reply, "USER") == 0) {
+                               *auth_supported |= NET_CLIENT_POP_AUTH_USER_PASS;
+                       } else if (strncmp(reply, "SASL ", 5U) == 0) {
+                               gchar **auth;
+                               guint n;
+
+                               auth = g_strsplit(&reply[5], " ", -1);
+                               for (n = 0U; auth[n] != NULL; n++) {
+                                       if (strcmp(auth[n], "PLAIN") == 0) {
+                                               *auth_supported |= NET_CLIENT_POP_AUTH_PLAIN;
+                                       } else if (strcmp(auth[n], "LOGIN") == 0) {
+                                               *auth_supported |= NET_CLIENT_POP_AUTH_LOGIN;
+                                       } else if (strcmp(auth[n], "CRAM-MD5") == 0) {
+                                               *auth_supported |= NET_CLIENT_POP_AUTH_CRAM_MD5;
+                                       } else if (strcmp(auth[n], "CRAM-SHA1") == 0) {
+                                               *auth_supported |= NET_CLIENT_POP_AUTH_CRAM_SHA1;
+                                       } else {
+                                               /* other auth methods are ignored for the time being */
+                                       }
+                               }
+                               g_strfreev(auth);
+                       } else if (strcmp(reply, "PIPELINING") == 0) {
+                               client->priv->can_pipelining = TRUE;
+                       } else if (strcmp(reply, "UIDL") == 0) {
+                               client->priv->can_uidl = TRUE;
+                       } else {
+                               /* ignore this capability (see MISRA C:2012, Rule 15.7) */
+                       }
+
+                       g_free(reply);
+               }
+       }
+
+       /* see RFC 1939, Sect. 4: if no other authentication method is supported explicitly (in particular no 
APOP), the server *must*
+        * at least support USER/PASS... */
+       if (*auth_supported == 0U) {
+               *auth_supported = NET_CLIENT_POP_AUTH_USER_PASS;
+       }client->priv->can_pipelining = TRUE;
+}
+
+
+static gboolean
+net_client_pop_uidl(NetClientPop *client, GList * const *msg_list, GError **error)
+{
+       gboolean result;
+       gboolean done;
+       const GList *p;
+
+       result = net_client_pop_execute(client, "UIDL", NULL, error);
+       done = FALSE;
+       p = *msg_list;
+       while (result && !done) {
+               gchar *reply;
+
+               result = net_client_read_line(NET_CLIENT(client), &reply, error);
+               if (result) {
+                       if (IS_ML_TERM(reply)) {
+                               done = TRUE;
+                       } else {
+                               guint msg_id;
+                               gchar *endptr;
+
+                               msg_id = strtoul(reply, &endptr, 10);
+                               if (endptr[0] != ' ') {
+                                       result = FALSE;
+                               } else {
+                                       /* we assume the passed list is already in the proper order, re-scan 
it if not */
+                                       if ((p == NULL) || (POP_MSG_INFO(p)->id != msg_id)) {
+                                               for (p = *msg_list; (p != NULL) && (POP_MSG_INFO(p)->id != 
msg_id); p = p->next) {
+                                                       /* nothing to do (see MISRA C:2012, Rule 15.7) */
+                                               }
+                                       }
+                                       /* FIXME - error if we get a UID for a message which is not in the 
list? */
+                                       if (p != NULL) {
+                                               NetClientPopMessageInfo* info = POP_MSG_INFO(p);
+
+                                               g_free(info->uid);
+                                               info->uid = g_strdup(&endptr[1]);
+                                               p = p->next;
+                                       }
+                               }
+                       }
+                       g_free(reply);
+               }
+       }
+
+       return result;
+}
+
+
+static gboolean
+net_client_pop_retr_msg(NetClientPop *client, const NetClientPopMessageInfo *info, NetClientPopMsgCb 
callback, gpointer user_data,
+                                               GError **error)
+{
+       gboolean result;
+       gboolean done;
+       GString *msg_buf;
+       gsize lines;
+
+       result = TRUE;
+       done = FALSE;
+       msg_buf = g_string_sized_new(POP_DATA_BUF_SIZE);
+       lines = 0U;
+       while (!done && result) {
+               gchar *linebuf;
+
+               result = net_client_read_line(NET_CLIENT(client), &linebuf, error);
+               if (result) {
+                       if (IS_ML_TERM(linebuf)) {
+                               done = TRUE;
+                       } else {
+                               if (linebuf[0] == '.') {
+                                       msg_buf = g_string_append(msg_buf, &linebuf[1]);
+                               } else {
+                                       msg_buf = g_string_append(msg_buf, linebuf);
+                               }
+                               msg_buf = g_string_append_c(msg_buf, '\n');
+                               lines++;
+
+                               /* pass an almost full buffer to the callback */
+                               if (msg_buf->len > (POP_DATA_BUF_SIZE - 100U)) {
+                                       result = callback(msg_buf->str, (gssize) msg_buf->len, lines, info, 
user_data, error);
+                                       msg_buf = g_string_truncate(msg_buf, 0U);
+                                       lines = 0U;
+                               }
+                       }
+                       g_free(linebuf);
+               }
+       }
+
+       if (result) {
+               if (msg_buf->len > 0U) {
+                       result = callback(msg_buf->str, (gssize) msg_buf->len, lines, info, user_data, error);
+               }
+               if (result) {
+                       result = callback(NULL, 0, 0U, info, user_data, error);
+               }
+       }
+
+       if (!result) {
+               (void) callback(NULL, -1, 0U, info, user_data, NULL);
+       }
+       (void) g_string_free(msg_buf, TRUE);
+
+       return result;
+
+}
diff --git a/libnetclient/net-client-pop.h b/libnetclient/net-client-pop.h
new file mode 100644
index 0000000..63ff340
--- /dev/null
+++ b/libnetclient/net-client-pop.h
@@ -0,0 +1,262 @@
+/* NetClient - simple line-based network client library
+ *
+ * Copyright (C) Albrecht Dreß <mailto:albrecht dress arcor de> 2017
+ *
+ * This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser 
General Public License
+ * as published by the Free Software Foundation; either version 3 of the License, or (at your option) any 
later version.
+ *
+ * This library is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even 
the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more 
details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License along with this library. If not, 
see
+ * <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef NET_CLIENT_POP_H_
+#define NET_CLIENT_POP_H_
+
+
+#include "net-client.h"
+
+
+G_BEGIN_DECLS
+
+
+#define NET_CLIENT_POP_TYPE                                    (net_client_pop_get_type())
+#define NET_CLIENT_POP(obj)                                    (G_TYPE_CHECK_INSTANCE_CAST((obj), 
NET_CLIENT_POP_TYPE, NetClientPop))
+#define NET_IS_CLIENT_POP(obj)                         (G_TYPE_CHECK_INSTANCE_TYPE((obj), 
NET_CLIENT_POP_TYPE))
+#define NET_CLIENT_POP_CLASS(klass)                    (G_TYPE_CHECK_CLASS_CAST((klass), 
NET_CLIENT_POP_TYPE, NetClientPopClass))
+#define NET_IS_CLIENT_POP_CLASS(klass)         (G_TYPE_CHECK_CLASS_TYPE((klass), NET_CLIENT_POP_TYPE))
+#define NET_CLIENT_POP_GET_CLASS(obj)          (G_TYPE_INSTANCE_GET_CLASS((obj), NET_CLIENT_POP_TYPE, 
NetClientPopClass))
+
+#define NET_CLIENT_POP_ERROR_QUARK                     (g_quark_from_static_string("net-client-pop"))
+
+
+typedef struct _NetClientPop NetClientPop;
+typedef struct _NetClientPopClass NetClientPopClass;
+typedef struct _NetClientPopPrivate NetClientPopPrivate;
+typedef struct _NetClientPopMessage NetClientPopMessage;
+typedef struct _NetClientPopMessageInfo NetClientPopMessageInfo;
+
+
+/** @brief POP-specific error codes */
+enum _NetClientPopError {
+       NET_CLIENT_ERROR_POP_PROTOCOL = 1,              /**< A bad server reply has been received. */
+       NET_CLIENT_ERROR_POP_SERVER_ERR,                /**< The server replied with an error. */
+       NET_CLIENT_ERROR_POP_NO_AUTH,                   /**< The server offers no suitable authentication 
mechanism. */
+       NET_CLIENT_ERROR_POP_NO_STARTTLS                /**< The server does not support STARTTLS. */
+};
+
+
+/** @name POP authentication methods
+ *
+ * Note that the availability of these authentication methods depends upon the result of the CAPABILITY 
list.  According to RFC
+ * 1939, Section 4, at least either APOP or USER/PASS @em must be supported.
+ * @{
+ */
+/** RFC 1939 "USER" and "PASS" authentication method. */
+#define NET_CLIENT_POP_AUTH_USER_PASS          0x01U
+/** RFC 1939 "APOP" authentication method. */
+#define NET_CLIENT_POP_AUTH_APOP                       0x02U
+/** RFC 5034 SASL "LOGIN" authentication method. */
+#define NET_CLIENT_POP_AUTH_LOGIN                      0x04U
+/** RFC 5034 SASL "PLAIN" authentication method. */
+#define NET_CLIENT_POP_AUTH_PLAIN                      0x08U
+/** RFC 5034 SASL "CRAM-MD5" authentication method. */
+#define NET_CLIENT_POP_AUTH_CRAM_MD5           0x10U
+/** RFC 5034 SASL "CRAM-SHA1" authentication method. */
+#define NET_CLIENT_POP_AUTH_CRAM_SHA1          0x20U
+/** Mask of all safe authentication methods, i.e. all methods which do not send the cleartext password. */
+#define NET_CLIENT_POP_AUTH_SAFE                       \
+       (NET_CLIENT_POP_AUTH_APOP + NET_CLIENT_POP_AUTH_CRAM_MD5 + NET_CLIENT_POP_AUTH_CRAM_SHA1)
+/** Mask of all authentication methods. */
+#define NET_CLIENT_POP_AUTH_ALL                                \
+       (NET_CLIENT_POP_AUTH_USER_PASS + NET_CLIENT_POP_AUTH_PLAIN + NET_CLIENT_POP_AUTH_LOGIN + 
NET_CLIENT_POP_AUTH_SAFE)
+/** @} */
+
+
+struct _NetClientPop {
+    NetClient parent;
+    NetClientPopPrivate *priv;
+};
+
+
+struct _NetClientPopClass {
+       NetClientClass parent;
+};
+
+
+/** @brief Message information
+ *
+ * This structure is returned in a GList by net_client_pop_list() and contains information about on message 
in the remote mailbox.
+ */
+struct _NetClientPopMessageInfo {
+       guint id;                                       /**< Message ID in the remote mailbox. */
+       gsize size;                                     /**< Size of the message in bytes. */
+       gchar *uid;                                     /**< Message UID, or NULL if it was not requested or 
the remote server does not support the UIDL
+                                                                * command. */
+};
+
+
+/** @brief POP3 Message Read Callback Function
+ *
+ * The user-provided callback function to receive a message from the remote POP3 server:
+ * - @em buffer - the next NUL-terminated chunk of data, always guaranteed to consist of complete, LF 
terminated lines, or NULL
+ *   when the @em count is <= 0 (see below)
+ * - @em count - indicates the number of bytes in the buffer (> 0), the end of the message (== 0), or that 
the download is
+ *   terminated due to an error condition (-1)
+ * - @em lines - number of lines in the buffer, valid only if @em count > 0
+ * - @em info - information for the current message
+ * - @em user_data - user data pointer
+ * - @em error - shall be filled with error information if an error occurs in the callback; this location is 
actually the @em error
+ *   parameter passed to net_client_pop_retr() with the exception of a call when the @em count is -1 when 
this parameter is always
+ *   NULL
+ * - return value: TRUE if the message download shall proceed, or FALSE to terminate it because an error 
occurred in the callback.
+ *   In the latter case, the callback function should set @em error appropriately.
+ *
+ * The message retrieved from the remote POP3 server is passed as "raw" data.  The line endings are always 
LF (i.e. @em not CRLF),
+ * and byte-stuffed termination '.' characters have been unstuffed.  If the data passed to the callback 
function shall be fed into
+ * <a href="http://spruce.sourceforge.net/gmime/";>GMime</a>, it is thus @em not necessary to run it through 
a GMimeFilterCRLF
+ * filter.
+ *
+ * The download of every message is terminated by calling the callback with a @em count of 0.  If the 
callback returns FALSE for a
+ * count >= 0, it is called again for the same message with count == -1 before the download is terminated.  
The return value of the
+ * callback called with count == -1 is ignored.
+ */
+typedef gboolean (*NetClientPopMsgCb)(const gchar *buffer, gssize count, gsize lines, const 
NetClientPopMessageInfo *info,
+                                                                         gpointer user_data, GError **error);
+
+
+GType net_client_pop_get_type(void)
+       G_GNUC_CONST;
+
+
+/** @brief Create a new POP network client
+ *
+ * @param host host name or IP address to connect
+ * @param port port number to connect
+ * @param crypt_mode encryption mode
+ * @param use_pipelining whether POP3 PIPELINING shall be used if supported by the remote server
+ * @return the POP network client object
+ */
+NetClientPop *net_client_pop_new(const gchar *host, guint16 port, NetClientCryptMode crypt_mode, gboolean 
use_pipelining);
+
+
+/** @brief Set allowed POP AUTH methods
+ *
+ * @param client POP network client object
+ * @param encrypted set allowed methods for encrypted or unencrypted connections
+ * @param allow_auth mask of allowed authentication methods
+ * @return TRUE on success or FALSE on error
+ *
+ * Set the allowed authentication methods for the passed connection.  The default is @ref 
NET_CLIENT_POP_AUTH_ALL for encrypted and
+ * @ref NET_CLIENT_POP_AUTH_SAFE for unencrypted connections, respectively.
+ *
+ * @note Call this function @em before calling net_client_pop_connect().
+ */
+gboolean net_client_pop_allow_auth(NetClientPop *client, gboolean encrypted, guint allow_auth);
+
+
+/** @brief Connect a POP network client
+ *
+ * @param client POP network client object
+ * @param greeting filled with the greeting of the POP server on success, may be NULL to ignore
+ * @param error filled with error information if the connection fails
+ * @return TRUE on success or FALSE if the connection failed
+ *
+ * Connect the remote POP server, initialise the encryption if requested, and emit the @ref auth signal to 
request authentication
+ * information.  Simply ignore the signal for an unauthenticated connection.  In order to shut down a 
successfully established
+ * connection, just call <tt>g_object_unref()</tt> on the POP network client object.
+ *
+ * @note The caller must free the returned greeting when it is not needed any more.
+ */
+gboolean net_client_pop_connect(NetClientPop *client, gchar **greeting, GError **error);
+
+
+/** @brief Get the status of a POP3 mailbox
+ *
+ * @param client POP network client object
+ * @param msg_count filled with the number of messages available in the mailbox, may be NULL to ignore the 
value
+ * @param mbox_size filled with the total mailbox size in bytes, may be NULL to ignore the value
+ * @param error filled with error information if the connection fails
+ * @return TRUE on success or FALSE if the command failed
+ *
+ * Run the POP3 STAT command to retrieve the mailbox status.
+ */
+gboolean net_client_pop_stat(NetClientPop *client, gsize *msg_count, gsize *mbox_size, GError **error);
+
+
+/** @brief List the messages in the POP3 mailbox
+ *
+ * @param client POP network client object
+ * @param msg_list filled with a list of @ref NetClientPopMessageInfo items
+ * @param with_uid TRUE to include the UID's of the messages in the returned list
+ * @param error filled with error information if the connection fails
+ * @return TRUE on success or FALSE if the command failed
+ *
+ * Run the LIST command and fill the passed list with the message identifier and message size for all 
messages available in the
+ * mailbox.  If the parameter @em with_uid is TRUE, also run the UIDL command and include the UID's reported 
by the remote server in
+ * the returned list.
+ *
+ * The caller shall free the items in the returned list by calling net_client_pop_msg_info_free() on them.
+ *
+ * @note The UID's can be added only if the remote server reports in its @em CAPABILITY list that the @em 
UIDL command is supported.
+ */
+gboolean net_client_pop_list(NetClientPop *client, GList **msg_list, gboolean with_uid, GError **error);
+
+
+/** @brief Load messages from the POP3 mailbox
+ *
+ * @param client POP network client object
+ * @param msg_list list of @ref NetClientPopMessageInfo items which shall be read from the server
+ * @param callback callback function which shall be called to process the downloaded message data
+ * @param user_data user data pointer passed to the callback function
+ * @param error filled with error information if the connection fails
+ * @return TRUE on success or FALSE if the command failed
+ *
+ * Load all messages in the passed list from the remote server, passing them through the specified callback 
function.  The function
+ * takes advantage of the RFC 2449 @em PIPELINING capability if supported by the remote server.
+ */
+gboolean net_client_pop_retr(NetClientPop *client, GList *msg_list, NetClientPopMsgCb callback, gpointer 
user_data, GError **error);
+
+
+/** @brief Delete messages from the POP3 mailbox
+ *
+ * @param client POP network client object
+ * @param msg_list list of @ref NetClientPopMessageInfo items which shall be deleted from the server
+ * @param error filled with error information if the connection fails
+ * @return TRUE on success or FALSE if the command failed
+ *
+ * Delete all messages in the passed list from the remote server.  The function takes advantage of the RFC 
2449 @em PIPELINING
+ * capability if supported by the remote server.
+ */
+gboolean net_client_pop_dele(NetClientPop *client, GList *msg_list, GError **error);
+
+
+/** @brief Free POP3 message item information
+ *
+ * @param info POP3 message item information as returned by net_client_pop_list()
+ *
+ * Free the data of a POP3 message item information.
+ */
+void net_client_pop_msg_info_free(NetClientPopMessageInfo *info);
+
+
+/** @file
+ *
+ * This module implements a POP3 client class conforming with <a 
href="https://tools.ietf.org/html/rfc1939";>RFC 1939</a>.
+ *
+ * The following features are supported:
+ * - the <i>STAT</i>, <i>LIST</i>, <i>RETR</i> and <i>DELE</i> commands as defined in RFC 1939;
+ * - support for <i>PIPELINING</i> and <i>UIDL</i> as defined by <a 
href="https://tools.ietf.org/html/rfc2449";>RFC 2449</a>;
+ * - <i>STLS</i> encryption as defined by <a href="https://tools.ietf.org/html/rfc2595";>RFC 2595</a>;
+ * - authentication using <i>APOP</i>, <i>USER/PASS</i> (both RFC 1939) or the SASL methods <i>PLAIN</i>, 
<i>LOGIN</i>,
+ *   <i>CRAM-MD5</i> or <i>CRAM-SHA1</i> (see <a href="https://tools.ietf.org/html/rfc5034";>RFC 5034</a>), 
depending upon the
+ *   capabilities reported by the server.
+ */
+
+
+G_END_DECLS
+
+
+#endif /* NET_CLIENT_POP_H_ */
diff --git a/libnetclient/net-client-smtp.c b/libnetclient/net-client-smtp.c
index 74c7858..b55710c 100644
--- a/libnetclient/net-client-smtp.c
+++ b/libnetclient/net-client-smtp.c
@@ -21,9 +21,7 @@
 struct _NetClientSmtpPrivate {
        NetClientCryptMode crypt_mode;
        guint auth_allowed[2];                  /** 0: encrypted, 1: unencrypted */
-       guint auth_supported;
        gboolean can_dsn;
-       gboolean can_starttls;
 };
 
 
@@ -52,11 +50,12 @@ G_DEFINE_TYPE(NetClientSmtp, net_client_smtp, NET_CLIENT_TYPE)
 
 
 static void net_client_smtp_finalise(GObject *object);
-static gboolean net_client_smtp_ehlo(NetClientSmtp *client, GError **error);
+static gboolean net_client_smtp_ehlo(NetClientSmtp *client, guint *auth_supported, gboolean *can_starttls, 
GError **error);
 static gboolean net_client_smtp_starttls(NetClientSmtp *client, GError **error);
 static gboolean net_client_smtp_execute(NetClientSmtp *client, const gchar *request_fmt, gchar **last_reply, 
GError **error, ...)
        G_GNUC_PRINTF(2, 5);
-static gboolean net_client_smtp_auth(NetClientSmtp *client, const gchar *user, const gchar *passwd, GError 
**error);
+static gboolean net_client_smtp_auth(NetClientSmtp *client, const gchar *user, const gchar *passwd, guint 
auth_supported,
+                                                                        GError **error);
 static gboolean net_client_smtp_auth_plain(NetClientSmtp *client, const gchar* user, const gchar* passwd, 
GError** error);
 static gboolean net_client_smtp_auth_login(NetClientSmtp *client, const gchar* user, const gchar* passwd, 
GError** error);
 static gboolean net_client_smtp_auth_cram(NetClientSmtp *client, GChecksumType chksum_type, const gchar 
*user, const gchar *passwd,
@@ -72,7 +71,8 @@ net_client_smtp_new(const gchar *host, guint16 port, NetClientCryptMode crypt_mo
 {
        NetClientSmtp *client;
 
-       g_return_val_if_fail(host != NULL, NULL);
+       g_return_val_if_fail((host != NULL) && (crypt_mode >= NET_CLIENT_CRYPT_ENCRYPTED) && (crypt_mode <= 
NET_CLIENT_CRYPT_NONE),
+               NULL);
 
        client = NET_CLIENT_SMTP(g_object_new(NET_CLIENT_SMTP_TYPE, NULL));
        if (client != NULL) {
@@ -106,6 +106,8 @@ gboolean
 net_client_smtp_connect(NetClientSmtp *client, gchar **greeting, GError **error)
 {
        gboolean result;
+       gboolean can_starttls = FALSE;
+       guint auth_supported = 0U;
 
        /* paranoia checks */
        g_return_val_if_fail(NET_IS_CLIENT_SMTP(client), FALSE);
@@ -123,13 +125,13 @@ net_client_smtp_connect(NetClientSmtp *client, gchar **greeting, GError **error)
 
        /* send EHLO and read the capabilities of the server */
        if (result) {
-               result = net_client_smtp_ehlo(client, error);
+               result = net_client_smtp_ehlo(client, &auth_supported, &can_starttls, error);
        }
 
        /* perform STARTTLS if required, and send EHLO again */
        if (result &&
                ((client->priv->crypt_mode == NET_CLIENT_CRYPT_STARTTLS) || (client->priv->crypt_mode == 
NET_CLIENT_CRYPT_STARTTLS_OPT))) {
-               if (!client->priv->can_starttls) {
+               if (!can_starttls) {
                        if (client->priv->crypt_mode == NET_CLIENT_CRYPT_STARTTLS) {
                                g_set_error(error, NET_CLIENT_SMTP_ERROR_QUARK, (gint) 
NET_CLIENT_ERROR_SMTP_NO_STARTTLS,
                                        _("remote server does not support STARTTLS"));
@@ -138,7 +140,7 @@ net_client_smtp_connect(NetClientSmtp *client, gchar **greeting, GError **error)
                } else {
                        result = net_client_smtp_starttls(client, error);
                        if (result) {
-                               result = net_client_smtp_ehlo(client, error);
+                               result = net_client_smtp_ehlo(client, &auth_supported, &can_starttls, error);
                        }
                }
        }
@@ -151,7 +153,7 @@ net_client_smtp_connect(NetClientSmtp *client, gchar **greeting, GError **error)
                g_debug("emit 'auth' signal for client %p", client);
                g_signal_emit_by_name(client, "auth", &auth_data);
                if ((auth_data != NULL) && (auth_data[0] != NULL) && (auth_data[1] != NULL)) {
-                       result = net_client_smtp_auth(client, auth_data[0], auth_data[1], error);
+                       result = net_client_smtp_auth(client, auth_data[0], auth_data[1], auth_supported, 
error);
                        memset(auth_data[0], 0, strlen(auth_data[0]));
                        memset(auth_data[1], 0, strlen(auth_data[1]));
                }
@@ -350,8 +352,6 @@ net_client_smtp_starttls(NetClientSmtp *client, GError **error)
 {
        gboolean result;
 
-       g_return_val_if_fail(NET_IS_CLIENT_SMTP(client), FALSE);
-
        result = net_client_smtp_execute(client, "STARTTLS", NULL, error);
        if (result) {
                result = net_client_start_tls(NET_CLIENT(client), error);
@@ -362,17 +362,17 @@ net_client_smtp_starttls(NetClientSmtp *client, GError **error)
 
 
 static gboolean
-net_client_smtp_auth(NetClientSmtp *client, const gchar *user, const gchar *passwd, GError **error)
+net_client_smtp_auth(NetClientSmtp *client, const gchar *user, const gchar *passwd, guint auth_supported, 
GError **error)
 {
        gboolean result;
        guint auth_mask;
 
-       g_return_val_if_fail(NET_IS_CLIENT_SMTP(client), FALSE);
+       g_return_val_if_fail(NET_IS_CLIENT_SMTP(client) && (user != NULL) && (passwd != NULL), FALSE);
 
        if (net_client_is_encrypted(NET_CLIENT(client))) {
-               auth_mask = client->priv->auth_allowed[0] & client->priv->auth_supported;
+               auth_mask = client->priv->auth_allowed[0] & auth_supported;
        } else {
-               auth_mask = client->priv->auth_allowed[1] & client->priv->auth_supported;
+               auth_mask = client->priv->auth_allowed[1] & auth_supported;
        }
 
        if ((auth_mask & NET_CLIENT_SMTP_AUTH_CRAM_SHA1) != 0U) {
@@ -418,8 +418,6 @@ net_client_smtp_auth_login(NetClientSmtp *client, const gchar *user, const gchar
        gboolean result;
        gchar *base64_buf;
 
-       g_return_val_if_fail((user != NULL) && (passwd != NULL), FALSE);
-
        base64_buf = g_base64_encode((const guchar *) user, strlen(user));
        result = net_client_smtp_execute(client, "AUTH LOGIN %s", NULL, error, base64_buf);
        memset(base64_buf, 0, strlen(base64_buf));
@@ -441,8 +439,6 @@ net_client_smtp_auth_cram(NetClientSmtp *client, GChecksumType chksum_type, cons
        gboolean result;
        gchar *challenge = NULL;
 
-       g_return_val_if_fail((user != NULL) && (passwd != NULL), FALSE);
-
        result = net_client_smtp_execute(client, "AUTH CRAM-%s", &challenge, error, 
net_client_chksum_to_str(chksum_type));
        if (result) {
                gchar *auth_buf;
@@ -462,6 +458,7 @@ net_client_smtp_auth_cram(NetClientSmtp *client, GChecksumType chksum_type, cons
 }
 
 
+/* note: if supplied, last_reply is never NULL on success */
 static gboolean
 net_client_smtp_execute(NetClientSmtp *client, const gchar *request_fmt, gchar **last_reply, GError **error, 
...)
 {
@@ -481,7 +478,7 @@ net_client_smtp_execute(NetClientSmtp *client, const gchar *request_fmt, gchar *
 
 
 static gboolean
-net_client_smtp_ehlo(NetClientSmtp *client, GError **error)
+net_client_smtp_ehlo(NetClientSmtp *client, guint *auth_supported, gboolean *can_starttls, GError **error)
 {
        gboolean result;
        gboolean done;
@@ -489,9 +486,9 @@ net_client_smtp_ehlo(NetClientSmtp *client, GError **error)
        result = net_client_write_line(NET_CLIENT(client), "EHLO %s", error, g_get_host_name());
 
        /* clear all capability flags */
-       client->priv->auth_supported = 0U;
+       *auth_supported = 0U;
        client->priv->can_dsn = FALSE;
-       client->priv->can_starttls = FALSE;
+       *can_starttls = FALSE;
 
        /* evaluate the response */
        done = FALSE;
@@ -512,7 +509,7 @@ net_client_smtp_ehlo(NetClientSmtp *client, GError **error)
                                if (strcmp(&endptr[1], "DSN") == 0) {
                                        client->priv->can_dsn = TRUE;
                                } else if (strcmp(&endptr[1], "STARTTLS") == 0) {
-                                       client->priv->can_starttls = TRUE;
+                                       *can_starttls = TRUE;
                                } else if ((strncmp(&endptr[1], "AUTH ", 5U) == 0) || (strncmp(&endptr[1], 
"AUTH=", 5U) == 0)) {
                                        gchar **auth;
                                        guint n;
@@ -520,20 +517,20 @@ net_client_smtp_ehlo(NetClientSmtp *client, GError **error)
                                        auth = g_strsplit(&endptr[6], " ", -1);
                                        for (n = 0U; auth[n] != NULL; n++) {
                                                if (strcmp(auth[n], "PLAIN") == 0) {
-                                                       client->priv->auth_supported |= 
NET_CLIENT_SMTP_AUTH_PLAIN;
+                                                       *auth_supported |= NET_CLIENT_SMTP_AUTH_PLAIN;
                                                } else if (strcmp(auth[n], "LOGIN") == 0) {
-                                                       client->priv->auth_supported |= 
NET_CLIENT_SMTP_AUTH_LOGIN;
+                                                       *auth_supported |= NET_CLIENT_SMTP_AUTH_LOGIN;
                                                } else if (strcmp(auth[n], "CRAM-MD5") == 0) {
-                                                       client->priv->auth_supported |= 
NET_CLIENT_SMTP_AUTH_CRAM_MD5;
+                                                       *auth_supported |= NET_CLIENT_SMTP_AUTH_CRAM_MD5;
                                                } else if (strcmp(auth[n], "CRAM-SHA1") == 0) {
-                                                       client->priv->auth_supported |= 
NET_CLIENT_SMTP_AUTH_CRAM_SHA1;
+                                                       *auth_supported |= NET_CLIENT_SMTP_AUTH_CRAM_SHA1;
                                                } else {
                                                        /* other auth methods are ignored for the time being 
*/
                                                }
                                        }
                                        g_strfreev(auth);
                                } else {
-                                       /* ignored */
+                                       /* ignored (see MISRA C:2012, Rule 15.7) */
                                }
 
                                if (*endptr == ' ') {
@@ -549,7 +546,7 @@ net_client_smtp_ehlo(NetClientSmtp *client, GError **error)
 }
 
 
-/* Note: according to RFC 5321, sect. 4.2, \em any reply may be multiline. */
+/* Note: according to RFC 5321, sect. 4.2, \em any reply may be multiline.  If supplied, last_reply is never 
NULL on success */
 static gboolean
 net_client_smtp_read_reply(NetClientSmtp *client, gint expect_code, gchar **last_reply, GError **error)
 {
diff --git a/libnetclient/net-client-smtp.h b/libnetclient/net-client-smtp.h
index b098d83..1eb35fc 100644
--- a/libnetclient/net-client-smtp.h
+++ b/libnetclient/net-client-smtp.h
@@ -44,7 +44,7 @@ enum _NetClientSmtpError {
        NET_CLIENT_ERROR_SMTP_PROTOCOL = 1,             /**< A bad server reply has been received. */
        NET_CLIENT_ERROR_SMTP_TRANSIENT,                /**< The server replied with a transient error code 
(code 4yz). */
        NET_CLIENT_ERROR_SMTP_PERMANENT,                /**< The server replied with a permanent error code 
(code 5yz). */
-       NET_CLIENT_ERROR_SMTP_NO_AUTH,          /**< The server offers no implemented authentication 
mechanism. */
+       NET_CLIENT_ERROR_SMTP_NO_AUTH,          /**< The server offers no suitable authentication mechanism. 
*/
        NET_CLIENT_ERROR_SMTP_NO_STARTTLS               /**< The server does not support STARTTLS. */
 };
 
@@ -118,7 +118,7 @@ GType net_client_smtp_get_type(void)
  * @param host host name or IP address to connect
  * @param port port number to connect
  * @param crypt_mode encryption mode
- * @return the net SMTP network client object
+ * @return the SMTP network client object
  */
 NetClientSmtp *net_client_smtp_new(const gchar *host, guint16 port, NetClientCryptMode crypt_mode);
 
diff --git a/libnetclient/net-client.c b/libnetclient/net-client.c
index 1250769..c7b73f7 100644
--- a/libnetclient/net-client.c
+++ b/libnetclient/net-client.c
@@ -51,7 +51,7 @@ net_client_new(const gchar *host_and_port, guint16 default_port, gsize max_line_
 {
        NetClient *client;
 
-       g_return_val_if_fail((host_and_port != NULL) && (max_line_len > 0U), NULL);
+       g_return_val_if_fail(host_and_port != NULL, NULL);
 
        client = NET_CLIENT(g_object_new(NET_CLIENT_TYPE, NULL));
 
@@ -73,7 +73,7 @@ net_client_configure(NetClient *client, const gchar *host_and_port, guint16 defa
        NetClientPrivate *priv;
        gboolean result;
 
-       g_return_val_if_fail(NET_IS_CLIENT(client) && (host_and_port != NULL) && (max_line_len > 0U), FALSE);
+       g_return_val_if_fail(NET_IS_CLIENT(client) && (host_and_port != NULL), FALSE);
 
        priv = client->priv;
        if (priv->plain_conn != NULL) {
@@ -118,6 +118,7 @@ net_client_connect(NetClient *client, GError **error)
        } else {
                priv->plain_conn = g_socket_client_connect_to_host(priv->sock, priv->host_and_port, 
priv->default_port, NULL, error);
                if (priv->plain_conn != NULL) {
+                       g_debug("connected to %s", priv->host_and_port);
                        priv->istream = 
g_data_input_stream_new(g_io_stream_get_input_stream(G_IO_STREAM(priv->plain_conn)));
                        g_data_input_stream_set_newline_type(priv->istream, G_DATA_STREAM_NEWLINE_TYPE_CR_LF);
                        priv->ostream = g_io_stream_get_output_stream(G_IO_STREAM(priv->plain_conn));
@@ -139,6 +140,7 @@ net_client_is_encrypted(NetClient *client)
        } else {
                result = FALSE;
        }
+
        return result;
 }
 
@@ -160,7 +162,7 @@ net_client_read_line(NetClient *client, gchar **recv_line, GError **error)
                line_buf = g_data_input_stream_read_line(client->priv->istream, &length, NULL, &read_err);
                if (line_buf != NULL) {
                        /* check that the protocol-specific maximum line length is not exceeded */
-                       if (length > client->priv->max_line_len) {
+                       if ((client->priv->max_line_len > 0U) && (length > client->priv->max_line_len)) {
                                g_set_error(error, NET_CLIENT_ERROR_QUARK, (gint) 
NET_CLIENT_ERROR_LINE_TOO_LONG,
                                        _("reply length %lu exceeds the maximum allowed length %lu"), length, 
client->priv->max_line_len);
                                g_free(line_buf);
@@ -224,7 +226,7 @@ net_client_vwrite_line(NetClient *client, const gchar *format, va_list args, GEr
        g_return_val_if_fail(NET_IS_CLIENT(client) && (format != NULL), FALSE);
 
        buf_len = g_vsnprintf(buffer, client->priv->max_line_len - 2U, format, args);
-       if ((buf_len < 0) || ((gsize) buf_len > (client->priv->max_line_len - 2U))) {
+       if ((buf_len < 0) || ((client->priv->max_line_len > 0U) && ((gsize) buf_len > 
(client->priv->max_line_len - 2U)))) {
                g_set_error(error, NET_CLIENT_ERROR_QUARK, (gint) NET_CLIENT_ERROR_LINE_TOO_LONG, _("line too 
long"));
                result = FALSE;
        } else {
@@ -299,7 +301,7 @@ net_client_set_cert_from_pem(NetClient *client, const gchar *pem_data, GError **
        } else {
                gnutls_datum_t data;
 
-               /*lint -e9005   cast'ing away the const is safe as gnutls treas data as const */
+               /*lint -e9005   cast'ing away the const is safe as gnutls treats data as const */
                data.data = (unsigned char *) pem_data;
                data.size = strlen(pem_data);
                res = gnutls_x509_crt_import(cert, &data, GNUTLS_X509_FMT_PEM);
@@ -501,6 +503,7 @@ net_client_finalise(GObject *object)
                g_object_unref(G_OBJECT(client->priv->certificate));
                client->priv->certificate = NULL;
        }
+       g_debug("finalised connection to %s", client->priv->host_and_port);
        g_free(client->priv->host_and_port);
        (*parent_class->finalize)(object);
 }
diff --git a/libnetclient/net-client.h b/libnetclient/net-client.h
index 41db07d..be89df1 100644
--- a/libnetclient/net-client.h
+++ b/libnetclient/net-client.h
@@ -80,7 +80,7 @@ GType net_client_get_type(void)
  *
  * @param host_and_port remote host and port or service, separated by a colon, which shall be connected
  * @param default_port default remote port if host_and_port does not contain a port
- * @param max_line_len maximum line length supported by the underlying protocol
+ * @param max_line_len maximum line length supported by the underlying protocol, 0 for no limit
  * @return the net network client object
  *
  * Create a new network client object with the passed parameters.  Call <tt>g_object_unref()</tt> on it to 
shut down the connection
@@ -94,7 +94,7 @@ NetClient *net_client_new(const gchar *host_and_port, guint16 default_port, gsiz
  * @param client network client
  * @param host_and_port remote host and port or service, separated by a colon, which shall be connected
  * @param default_port default remote port if host_and_port does not contain a port
- * @param max_line_len maximum line length supported by the underlying protocol
+ * @param max_line_len maximum line length supported by the underlying protocol, 0 for no limit
  * @param error filled with error information on error
  * @return TRUE is the connection was successful, FALSE on error
  *
@@ -184,7 +184,8 @@ gboolean net_client_start_tls(NetClient *client, GError **error);
  *
  * Read a CRLF-terminated line from the remote server and return it in the passed buffer.  The terminating 
CRLF is always stripped.
  *
- * @note The caller must free the returned buffer when it is not needed any more.
+ * @note If supplied, the response buffer is never NULL on success.  The caller must free the returned 
buffer when it is not needed
+ *       any more.
  */
 gboolean net_client_read_line(NetClient *client, gchar **recv_line, GError **error);
 
@@ -263,8 +264,9 @@ gboolean net_client_set_timeout(NetClient *client, guint timeout_secs);
  * @mainpage
  *
  * This library provides an implementation of CRLF-terminated line-based client protocols built on top of 
GIO.  It provides a base
- * module (see file net-client.h), containing the line-based IO methods, and on top of that a SMTP (RFC5321) 
client class (see
- * file net-client-smtp.h).  The file net-client-utils.h contains some helper functions for authentication.
+ * module (see file net-client.h), containing the line-based IO methods, and on top of that SMTP (RFC 5321) 
and POP3 (RFC 1939)
+ * client classes (see files net-client-smtp.h and net-client-pop.h, respectively).  The file 
net-client-utils.h contains some
+ * helper functions for authentication.
  *
  * \author Written by Albrecht Dreß mailto:albrecht dress arcor de
  * \copyright Copyright &copy; Albrecht Dreß 2017<br/>
diff --git a/libnetclient/test/Makefile.am b/libnetclient/test/Makefile.am
index be9caf7..d735dc5 100644
--- a/libnetclient/test/Makefile.am
+++ b/libnetclient/test/Makefile.am
@@ -2,7 +2,7 @@
 
 # Note: the following hack is needed so lcov recognises the paths of the sources...
 libsrcdir = $(shell echo $(abs_srcdir) | sed -e 's;/test$$;;')
-test_src = $(libsrcdir)/net-client.c $(libsrcdir)/net-client-smtp.c $(libsrcdir)/net-client-utils.c
+test_src = $(libsrcdir)/net-client.c $(libsrcdir)/net-client-pop.c $(libsrcdir)/net-client-smtp.c 
$(libsrcdir)/net-client-utils.c
 
 EXTRA_DIST =           \
        tests.c                 \
@@ -16,7 +16,8 @@ TESTFLAGS = -DNCAT="\"@NCAT@\"" -DSED="\"@SED@\"" -fprofile-arcs -ftest-coverage
 
 LCOVFLGS       = --rc lcov_branch_coverage=1
 GENHTMLFLGS    = --function-coverage --branch-coverage --num-spaces 4
-VALGRFLAGS  = --tool=memcheck --log-file=$@.vg --suppressions=valgrind.supp --leak-check=full 
--child-silent-after-fork=yes
+VALGRFLAGS  = --tool=memcheck --log-file=$@.vg --suppressions=valgrind.supp --leak-check=full 
--track-fds=yes \
+                         --child-silent-after-fork=yes
 
 CLEANFILES = *.gcda *.gcno *.covi *.vg tests
 
diff --git a/libnetclient/test/inetsim-1.2.6-POP3.diff b/libnetclient/test/inetsim-1.2.6-POP3.diff
new file mode 100644
index 0000000..6271880
--- /dev/null
+++ b/libnetclient/test/inetsim-1.2.6-POP3.diff
@@ -0,0 +1,45 @@
+--- inetsim-1.2.6-orig/lib/INetSim/POP3.pm     2016-08-29 09:43:28.000000000 +0200
++++ inetsim-1.2.6/lib/INetSim/POP3.pm  2017-03-20 18:47:24.082678213 +0100
+@@ -32,7 +32,7 @@
+                       "SASL"                  =>      2,      # RFC 2449, 1734, 5034, 2195 ... 
(http://www.iana.org/assignments/sasl-mechanisms)
+                       "RESP-CODES"            =>      1,      # RFC 2449
+                       "LOGIN-DELAY"           =>      2,      # RFC 2449
+-                      "PIPELINING"            =>      0,      # RFC 2449
++                      "PIPELINING"            =>      1,      # RFC 2449
+                       "EXPIRE"                =>      2,      # RFC 2449
+                       "UIDL"                  =>      1,      # RFC 1939, 2449
+                       "IMPLEMENTATION"        =>      2,      # RFC 2449
+@@ -292,6 +292,11 @@
+               $line =~ s/[\r\n\s\t]+$//g;
+               alarm($self->{timeout});
+               $self->slog_("recv: $line");
++              ### flush input buffer if pipelining is disabled
++              if (!defined $POP3_CAPA{PIPELINING}) {
++
++                  ### FIXME - flush any pending input here
++              }
+               ### Auth via USER/PASS
+               if ($line =~ /^USER(|([\s]+)(.*))$/i && defined $POP3_CAPA{USER}) {
+                   $self->USER($3);
+@@ -1038,9 +1043,11 @@
+     my ($flag, $hash, $uid, $size, $header, $body) = $self->read_mail($args);
+     if (defined $flag && $flag) {
+         $self->send_("+OK", "Message follows ($size octets)");
++        # quote termination octet (RFC 1939, Sect. 3)
++        $body =~ s/\r\n\./\r\n../g;
+         print $client "$header\r\n$body";
+         $self->slog_("send: <(MESSAGE)>");
+-        print $client "\r\n.\r\n";
++        print $client ".\r\n";
+         $self->slog_("send: .");
+         $status{retrieved}++;
+     }
+@@ -1300,8 +1307,6 @@
+     # convert LF to CR/LF
+     $msg =~ s/\r\n/\n/g;
+     $msg =~ s/\n/\r\n/g;
+-    # quote 'CR+LF+.+CR+LF'
+-    $msg =~ s/\r\n\.\r\n/\r\n\.\.\r\n/g;
+     # split header & body
+     $msg =~ s/(\r\n){2,}/\|/;
+     ($header, $body) = split(/\|/, $msg, 2);
diff --git a/libnetclient/test/inetsim.conf b/libnetclient/test/inetsim.conf
index d3ab91b..75e5fa0 100644
--- a/libnetclient/test/inetsim.conf
+++ b/libnetclient/test/inetsim.conf
@@ -888,7 +888,7 @@ smtps_auth_required yes
 #
 # Default: 110
 #
-#pop3_bind_port                110
+pop3_bind_port         64110
 
 
 #########################################
@@ -1021,6 +1021,7 @@ pop3_capability           SASL PLAIN LOGIN ANONYMOUS CRAM-MD5 CRAM-SHA1
 pop3_capability                UIDL
 pop3_capability                IMPLEMENTATION "INetSim POP3 server"
 pop3_capability                STLS
+pop3_capability                PIPELINING
 #
 
 
@@ -1090,7 +1091,7 @@ pop3_capability           STLS
 #
 # Default: 995
 #
-#pop3s_bind_port               995
+pop3s_bind_port                64995
 
 
 #########################################
@@ -1165,7 +1166,7 @@ pop3_capability           STLS
 #
 # Default: yes
 #
-#pop3s_enable_apop     no
+pop3s_enable_apop      no
 
 
 #########################################
diff --git a/libnetclient/test/tests.c b/libnetclient/test/tests.c
index d636a0b..053d48c 100644
--- a/libnetclient/test/tests.c
+++ b/libnetclient/test/tests.c
@@ -11,12 +11,14 @@
 #include <sput.h>
 #include "net-client.h"
 #include "net-client-smtp.h"
+#include "net-client-pop.h"
 #include "net-client-utils.h"
 
 
 static void test_basic(void);
 static void test_basic_crypt(void);
 static void test_smtp(void);
+static void test_pop3(void);
 static void test_utils(void);
 
 
@@ -34,8 +36,8 @@ main(G_GNUC_UNUSED int argc, G_GNUC_UNUSED char **argv)
        sput_enter_suite("test SMTP");
        sput_run_test(test_smtp);
 
-       //sput_enter_suite("test POP3");
-       //sput_run_test(test_pop3);
+       sput_enter_suite("test POP3");
+       sput_run_test(test_pop3);
 
        sput_enter_suite("test utility functions");
        sput_run_test(test_utils);
@@ -57,7 +59,6 @@ test_basic(void)
        gchar *read_res;
 
        sput_fail_unless(net_client_new(NULL, 65000, 42) == NULL, "missing host");
-       sput_fail_unless(net_client_new("localhost", 65000, 0) == NULL, "zero max line length");
 
        sput_fail_unless((basic = net_client_new("localhost", 65000, 42)) != NULL, "localhost; port 65000");
        sput_fail_unless(net_client_get_host(NULL) == NULL, "get host w/o client");
@@ -68,7 +69,6 @@ test_basic(void)
        sput_fail_unless((basic = net_client_new("www.google.com", 80, 1)) != NULL, "www.google.com:80; port 
0");
        sput_fail_unless(net_client_configure(NULL, "localhost", 65000, 42, NULL) == FALSE, "configure w/o 
client");
        sput_fail_unless(net_client_configure(basic, NULL, 65000, 42, NULL) == FALSE, "configure w/o host");
-       sput_fail_unless(net_client_configure(basic, "localhost", 65000, 0, NULL) == FALSE, "configure w/ 
zero max line length");
        sput_fail_unless(net_client_configure(basic, "localhost", 65000, 42, NULL) == TRUE, "configure 
localhost:65000 ok");
 
        sput_fail_unless(net_client_set_timeout(NULL, 3) == FALSE, "set timeout w/o client");
@@ -78,7 +78,7 @@ test_basic(void)
        sput_fail_unless((op_res == FALSE) && (error->code == NET_CLIENT_ERROR_NOT_CONNECTED), "write w/o 
connection");
        g_clear_error(&error);
        op_res =  net_client_read_line(basic, NULL, &error);
-       sput_fail_unless((op_res == FALSE) && (error->code == NET_CLIENT_ERROR_NOT_CONNECTED), "read w/o 
connection");
+       sput_fail_unless((op_res == FALSE) && (error->code == NET_CLIENT_ERROR_NOT_CONNECTED), "read line w/o 
connection");
        g_clear_error(&error);
 
        op_res =
@@ -108,11 +108,10 @@ test_basic(void)
        g_clear_error(&error);
        sput_fail_unless(net_client_write_line(basic, "%s", NULL, "x") == TRUE, "write ok");
 
-       sput_fail_unless(net_client_read_line(NULL, NULL, NULL) == FALSE, "read w/o client");
-       sput_fail_unless(net_client_read_line(basic, NULL, NULL) == TRUE, "read, data discarded");
+       sput_fail_unless(net_client_read_line(NULL, NULL, NULL) == FALSE, "read line w/o client");
+       sput_fail_unless(net_client_read_line(basic, NULL, NULL) == TRUE, "read line, data discarded");
        op_res = net_client_read_line(basic, NULL, &error);
-       sput_fail_unless((op_res == FALSE) && (error->code == G_IO_ERROR_TIMED_OUT), "read timeout");
-       g_message("%s %d %s", g_quark_to_string(error->domain), error->code, error->message);
+       sput_fail_unless((op_res == FALSE) && (error->code == G_IO_ERROR_TIMED_OUT), "read line timeout");
        g_clear_error(&error);
 
        sput_fail_unless(net_client_execute(NULL, NULL, "Hi There", NULL) == FALSE, "execute w/o client");
@@ -285,7 +284,9 @@ get_auth(NetClient *client, gpointer user_data)
        g_message("%s(%p, %p)", __func__, client, user_data);
        result = g_new0(gchar *, 3U);
        result[0] = g_strdup("john.doe");
-       result[1] = g_strdup("@ C0mplex P@sswd");
+       if (user_data != NULL) {
+               result[1] = g_strdup("@ C0mplex P@sswd");
+       }
        return result;
 }
 
@@ -318,7 +319,9 @@ test_smtp(void)
 
 
        // smtp stuff - test various failures
-       sput_fail_unless(net_client_smtp_new(NULL, 0, NET_CLIENT_CRYPT_NONE) == NULL, "missing host");
+       sput_fail_unless(net_client_smtp_new(NULL, 0, NET_CLIENT_CRYPT_NONE) == NULL, "new, missing host");
+       sput_fail_unless(net_client_smtp_new("localhost", 0, 0) == NULL, "new, bad crypt mode");
+       sput_fail_unless(net_client_smtp_new("localhost", 0, 42) == NULL, "new, bad crypt mode");
 
        sput_fail_unless((smtp = net_client_smtp_new("localhost", 65000, NET_CLIENT_CRYPT_NONE)) != NULL, 
"localhost; port 65000");
        sput_fail_unless(net_client_smtp_connect(smtp, NULL, NULL) == FALSE, "no server");
@@ -364,6 +367,12 @@ test_smtp(void)
        g_clear_error(&error);
        g_object_unref(smtp);
 
+       // no password: anonymous
+       sput_fail_unless((smtp = net_client_smtp_new("localhost", 65025, NET_CLIENT_CRYPT_NONE)) != NULL, 
"localhost:65025");
+       g_signal_connect(G_OBJECT(smtp), "auth", G_CALLBACK(get_auth), NULL);
+       sput_fail_unless(net_client_smtp_connect(smtp, NULL, NULL), "connect: anonymous ok (NULL passwd)");
+       g_object_unref(smtp);
+
        // unencrypted, PLAIN auth
        sput_fail_unless((smtp = net_client_smtp_new("localhost", 65025, NET_CLIENT_CRYPT_NONE)) != NULL, 
"localhost:65025");
        sput_fail_unless(net_client_smtp_allow_auth(NULL, FALSE, NET_CLIENT_SMTP_AUTH_PLAIN) == FALSE, "set 
auth meths, no client");
@@ -426,6 +435,226 @@ test_smtp(void)
        net_client_smtp_msg_free(msg);
 }
 
+static gboolean
+msg_cb(const gchar *buffer, gssize count, gsize lines, const NetClientPopMessageInfo *info, gpointer 
user_data, GError **error)
+{
+       g_message("%s(%p, %ld, %lu, %p, %p, %p)", __func__, buffer, count, lines, info, user_data, error);
+       if (((GPOINTER_TO_INT(user_data) == 1) && (count > 0)) ||
+               ((GPOINTER_TO_INT(user_data) == 2) && (count == 0))) {
+               return FALSE;
+       } else {
+               return TRUE;
+       }
+}
+
+static void
+test_pop3(void)
+{
+       NetClientPop *pop;
+       GError *error = NULL;
+       gboolean op_res;
+       gchar *read_res;
+       gsize msg_count;
+       gsize mbox_size;
+       GList *msg_list;
+
+       // some error cases
+       sput_fail_unless(net_client_pop_new(NULL, 0, NET_CLIENT_CRYPT_NONE, TRUE) == NULL, "new, missing 
host");
+       sput_fail_unless(net_client_pop_new("localhost", 0, 0, TRUE) == NULL, "new, bad crypt mode");
+       sput_fail_unless(net_client_pop_new("localhost", 0, 42, TRUE) == NULL, "new, bad crypt mode");
+       sput_fail_unless(net_client_pop_allow_auth(NULL, TRUE, 0U) == FALSE, "allow auth, no client");
+       sput_fail_unless(net_client_pop_connect(NULL, NULL, NULL) == FALSE, "connect, no client");
+       sput_fail_unless(net_client_pop_stat(NULL, NULL, NULL, NULL) == FALSE, "stat, no client");
+       sput_fail_unless(net_client_pop_list(NULL, NULL, FALSE, NULL) == FALSE, "list, no client");
+       sput_fail_unless(net_client_pop_retr(NULL, NULL, NULL, NULL, NULL) == FALSE, "retr, no client");
+       sput_fail_unless(net_client_pop_dele(NULL, NULL, NULL) == FALSE, "dele, no client");
+       net_client_pop_msg_info_free(NULL);             // just for checking
+
+       // some basic stuff
+       sput_fail_unless((pop = net_client_pop_new("localhost", 65000, NET_CLIENT_CRYPT_NONE, TRUE)) != NULL, 
"localhost; port 65000");
+       sput_fail_unless(net_client_pop_connect(pop, NULL, NULL) == FALSE, "no server");
+       g_object_unref(pop);
+
+       sput_fail_unless((pop = net_client_pop_new("localhost", 64110, NET_CLIENT_CRYPT_NONE, TRUE)) != NULL, 
"localhost:64110");
+       op_res = net_client_pop_connect(pop, &read_res, NULL);
+       sput_fail_unless((op_res == TRUE) && (strncmp(read_res, "INetSim POP3 Server ready <", 27U) == 0),
+               "connect: success");
+       g_free(read_res);
+       sput_fail_unless(net_client_is_encrypted(NET_CLIENT(pop)) == FALSE, "not encrypted");
+       op_res = net_client_pop_connect(pop, NULL, &error);
+       sput_fail_unless((op_res == FALSE) && (error->code == NET_CLIENT_ERROR_CONNECTED), "cannot 
reconnect");
+       g_clear_error(&error);
+
+       // not allowed if unauthenticated
+       op_res = net_client_pop_stat(pop, NULL, NULL, &error);
+       sput_fail_unless((op_res == FALSE) && (error->code == NET_CLIENT_ERROR_POP_SERVER_ERR), "STAT not 
allowed w/o AUTH");
+       g_clear_error(&error);
+       g_object_unref(pop);
+
+       sput_fail_unless((pop = net_client_pop_new("localhost", 64110, NET_CLIENT_CRYPT_NONE, TRUE)) != NULL, 
"localhost:64110");
+       g_signal_connect(G_OBJECT(pop), "auth", G_CALLBACK(get_auth), NULL);
+       sput_fail_unless(net_client_pop_connect(pop, NULL, NULL) == TRUE, "connect: success");
+       sput_fail_unless(net_client_pop_list(pop, NULL, TRUE, NULL) == FALSE, "list w/ empty target list");
+       op_res = net_client_pop_list(pop, &msg_list, TRUE, &error);
+       sput_fail_unless((op_res == FALSE) && (error->code == NET_CLIENT_ERROR_POP_SERVER_ERR), "LIST not 
allowed w/ empty AUTH");
+       g_clear_error(&error);
+       g_object_unref(pop);
+
+       // unencrypted, force USER auth
+       sput_fail_unless((pop = net_client_pop_new("localhost", 64110, NET_CLIENT_CRYPT_NONE, TRUE)) != NULL, 
"localhost:64110");
+       sput_fail_unless(net_client_pop_allow_auth(pop, FALSE, 0U), "no AUTH mechanism");
+       g_signal_connect(G_OBJECT(pop), "auth", G_CALLBACK(get_auth), pop);
+       op_res = net_client_pop_connect(pop, NULL, &error);
+       sput_fail_unless((op_res == FALSE) && (error->code == NET_CLIENT_ERROR_POP_NO_AUTH), "no suitable 
AUTH mechanism");
+       g_clear_error(&error);
+       g_object_unref(pop);
+
+       // STARTTLS
+       sput_fail_unless((pop = net_client_pop_new("localhost", 64110, NET_CLIENT_CRYPT_STARTTLS, TRUE)) != 
NULL,
+               "localhost:64110, starttls");
+       op_res = net_client_pop_connect(pop, NULL, &error);
+       sput_fail_unless((op_res == FALSE) && (error != NULL), "connect: fails (untrusted cert)");
+       g_clear_error(&error);
+       g_object_unref(pop);
+
+       sput_fail_unless((pop = net_client_pop_new("localhost", 64110, NET_CLIENT_CRYPT_STARTTLS_OPT, TRUE)) 
!= NULL,
+               "localhost:64110, starttls (opt)");
+       op_res = net_client_pop_connect(pop, NULL, &error);
+       sput_fail_unless((op_res == TRUE) && (error == NULL), "connect: ok, but...");
+       sput_fail_unless(net_client_is_encrypted(NET_CLIENT(pop)) == FALSE, "...not encrypted");
+       g_object_unref(pop);
+
+       // STARTTLS required, USER/PASS auth
+       sput_fail_unless((pop = net_client_pop_new("localhost", 64110, NET_CLIENT_CRYPT_STARTTLS, FALSE)) != 
NULL,
+               "localhost:64110, starttls, no pipelining");
+       sput_fail_unless(net_client_pop_allow_auth(pop, TRUE, NET_CLIENT_POP_AUTH_USER_PASS) == TRUE, "force 
auth meth USER/PASS");
+       g_signal_connect(G_OBJECT(pop), "cert-check", G_CALLBACK(check_cert), NULL);
+       g_signal_connect(G_OBJECT(pop), "auth", G_CALLBACK(get_auth), pop);
+       sput_fail_unless(net_client_pop_connect(pop, NULL, NULL) == TRUE, "connect: success");
+       sput_fail_unless(net_client_pop_stat(pop, &msg_count, NULL, NULL) == TRUE, "STAT: success");
+       sput_fail_unless(net_client_pop_list(pop, &msg_list, FALSE, NULL) == TRUE, "LIST: success");
+       g_message("message count: %u", g_list_length(msg_list));
+       if (msg_list != NULL) {
+               sput_fail_unless(net_client_pop_retr(pop, NULL, msg_cb, NULL, NULL) == FALSE, "retr w/o 
message list");
+               sput_fail_unless(net_client_pop_retr(pop, msg_list, NULL, NULL, NULL) == FALSE, "retr w/o 
callback");
+               sput_fail_unless(net_client_pop_retr(pop, msg_list, msg_cb, GINT_TO_POINTER(1), NULL) == 
FALSE, "retr error");
+               g_list_free_full(msg_list, (GDestroyNotify) net_client_pop_msg_info_free);
+       }
+       g_object_unref(pop);
+
+       // STARTTLS optional, APOP auth
+       sput_fail_unless((pop = net_client_pop_new("localhost", 64110, NET_CLIENT_CRYPT_STARTTLS_OPT, TRUE)) 
!= NULL,
+               "localhost:64110, starttls opt, pipelining");
+       sput_fail_unless(net_client_pop_allow_auth(pop, TRUE, NET_CLIENT_POP_AUTH_APOP) == TRUE, "force auth 
meth APOP");
+       g_signal_connect(G_OBJECT(pop), "cert-check", G_CALLBACK(check_cert), NULL);
+       g_signal_connect(G_OBJECT(pop), "auth", G_CALLBACK(get_auth), pop);
+       sput_fail_unless(net_client_pop_connect(pop, NULL, NULL) == TRUE, "connect: success");
+       sput_fail_unless(net_client_pop_stat(pop, NULL, &mbox_size, NULL) == TRUE, "STAT: success");
+       sput_fail_unless(net_client_pop_list(pop, &msg_list, FALSE, NULL) == TRUE, "LIST: success");
+       g_message("message count: %u", g_list_length(msg_list));
+       if (msg_list != NULL) {
+               sput_fail_unless(net_client_pop_retr(pop, msg_list, msg_cb, GINT_TO_POINTER(2), NULL) == 
FALSE, "retr error");
+               g_list_free_full(msg_list, (GDestroyNotify) net_client_pop_msg_info_free);
+       }
+       g_object_unref(pop);
+
+       // STARTTLS required, PLAIN auth
+       sput_fail_unless((pop = net_client_pop_new("localhost", 64110, NET_CLIENT_CRYPT_STARTTLS, TRUE)) != 
NULL,
+               "localhost:64110, starttls, pipelining");
+       sput_fail_unless(net_client_pop_allow_auth(pop, TRUE, NET_CLIENT_POP_AUTH_PLAIN) == TRUE, "force auth 
meth PLAIN");
+       g_signal_connect(G_OBJECT(pop), "cert-check", G_CALLBACK(check_cert), NULL);
+       g_signal_connect(G_OBJECT(pop), "auth", G_CALLBACK(get_auth), pop);
+       sput_fail_unless(net_client_pop_connect(pop, NULL, NULL) == TRUE, "connect: success");
+       sput_fail_unless(net_client_pop_list(pop, &msg_list, FALSE, NULL) == TRUE, "LIST: success");
+       g_message("message count: %u", g_list_length(msg_list));
+       if (msg_list != NULL) {
+               sput_fail_unless(net_client_pop_retr(pop, msg_list, msg_cb, NULL, NULL) == TRUE, "retr ok");
+               g_list_free_full(msg_list, (GDestroyNotify) net_client_pop_msg_info_free);
+       }
+       g_object_unref(pop);
+
+       // STARTTLS optional, PLAIN auth
+       sput_fail_unless((pop = net_client_pop_new("localhost", 64110, NET_CLIENT_CRYPT_STARTTLS_OPT, FALSE)) 
!= NULL,
+               "localhost:64110, starttls opt, no pipelining");
+       sput_fail_unless(net_client_pop_allow_auth(pop, TRUE, NET_CLIENT_POP_AUTH_PLAIN) == TRUE, "force auth 
meth PLAIN");
+       g_signal_connect(G_OBJECT(pop), "cert-check", G_CALLBACK(check_cert), NULL);
+       g_signal_connect(G_OBJECT(pop), "auth", G_CALLBACK(get_auth), pop);
+       sput_fail_unless(net_client_pop_connect(pop, NULL, NULL) == TRUE, "connect: success");
+       sput_fail_unless(net_client_pop_list(pop, &msg_list, FALSE, NULL) == TRUE, "LIST: success");
+       g_message("message count: %u", g_list_length(msg_list));
+       if (msg_list != NULL) {
+               sput_fail_unless(net_client_pop_retr(pop, msg_list, msg_cb, NULL, NULL) == TRUE, "retr ok");
+               g_list_free_full(msg_list, (GDestroyNotify) net_client_pop_msg_info_free);
+       }
+       g_object_unref(pop);
+
+       // SSL, LOGIN auth
+       sput_fail_unless((pop = net_client_pop_new("localhost", 64995, NET_CLIENT_CRYPT_ENCRYPTED, FALSE)) != 
NULL,
+               "localhost:64995, pop3s, no pipelining");
+       sput_fail_unless(net_client_pop_allow_auth(pop, TRUE, NET_CLIENT_POP_AUTH_LOGIN) == TRUE, "force auth 
meth LOGIN");
+       g_signal_connect(G_OBJECT(pop), "cert-check", G_CALLBACK(check_cert), NULL);
+       g_signal_connect(G_OBJECT(pop), "auth", G_CALLBACK(get_auth), pop);
+       sput_fail_unless(net_client_pop_connect(pop, NULL, &error) == TRUE, "connect: success");
+       sput_fail_unless(net_client_pop_stat(pop, &msg_count, &mbox_size, NULL) == TRUE, "STAT: success");
+       sput_fail_unless(net_client_pop_list(pop, &msg_list, FALSE, NULL) == TRUE, "LIST: success");
+       g_message("message count: %u", g_list_length(msg_list));
+       if (msg_list != NULL) {
+               sput_fail_unless(net_client_pop_retr(pop, msg_list, msg_cb, GINT_TO_POINTER(1), NULL) == 
FALSE, "retr error");
+               g_list_free_full(msg_list, (GDestroyNotify) net_client_pop_msg_info_free);
+       }
+       g_object_unref(pop);
+
+       // SSL, CRAM-MD5 auth
+       sput_fail_unless((pop = net_client_pop_new("localhost", 64995, NET_CLIENT_CRYPT_ENCRYPTED, TRUE)) != 
NULL,
+               "localhost:64995, pop3s, pipelining");
+       sput_fail_unless(net_client_pop_allow_auth(pop, TRUE, NET_CLIENT_POP_AUTH_CRAM_MD5) == TRUE, "force 
auth meth CRAM-MD5");
+       g_signal_connect(G_OBJECT(pop), "cert-check", G_CALLBACK(check_cert), NULL);
+       g_signal_connect(G_OBJECT(pop), "auth", G_CALLBACK(get_auth), pop);
+       sput_fail_unless(net_client_pop_connect(pop, NULL, NULL) == TRUE, "connect: success");
+       sput_fail_unless(net_client_pop_list(pop, &msg_list, TRUE, NULL) == TRUE, "LIST: success");
+       g_message("message count: %u", g_list_length(msg_list));
+       if (msg_list != NULL) {
+               sput_fail_unless(net_client_pop_retr(pop, msg_list, msg_cb, GINT_TO_POINTER(2), NULL) == 
FALSE, "retr error");
+               g_list_free_full(msg_list, (GDestroyNotify) net_client_pop_msg_info_free);
+       }
+       g_object_unref(pop);
+
+       // SSL, CRAM-SHA1 auth
+       sput_fail_unless((pop = net_client_pop_new("localhost", 64995, NET_CLIENT_CRYPT_ENCRYPTED, FALSE)) != 
NULL,
+               "localhost:64995, pop3s, no pipelining");
+       sput_fail_unless(net_client_pop_allow_auth(pop, TRUE, NET_CLIENT_POP_AUTH_CRAM_SHA1) == TRUE, "force 
auth meth CRAM-SHA1");
+       g_signal_connect(G_OBJECT(pop), "cert-check", G_CALLBACK(check_cert), NULL);
+       g_signal_connect(G_OBJECT(pop), "auth", G_CALLBACK(get_auth), pop);
+       sput_fail_unless(net_client_pop_connect(pop, NULL, &error) == TRUE, "connect: success");
+       sput_fail_unless(net_client_pop_list(pop, &msg_list, TRUE, NULL) == TRUE, "LIST: success");
+       g_message("message count: %u", g_list_length(msg_list));
+       if (msg_list != NULL) {
+               sput_fail_unless(net_client_pop_retr(pop, msg_list, msg_cb, NULL, NULL) == TRUE, "retr ok");
+               g_list_free_full(msg_list, (GDestroyNotify) net_client_pop_msg_info_free);
+       }
+       g_object_unref(pop);
+
+       // SSL, CRAM-SHA1 auth
+       sput_fail_unless((pop = net_client_pop_new("localhost", 64995, NET_CLIENT_CRYPT_ENCRYPTED, TRUE)) != 
NULL,
+               "localhost:64995, pop3s, pipelining");
+       sput_fail_unless(net_client_pop_allow_auth(pop, TRUE, NET_CLIENT_POP_AUTH_CRAM_SHA1) == TRUE, "force 
auth meth CRAM-SHA1");
+       g_signal_connect(G_OBJECT(pop), "cert-check", G_CALLBACK(check_cert), NULL);
+       g_signal_connect(G_OBJECT(pop), "auth", G_CALLBACK(get_auth), pop);
+       sput_fail_unless(net_client_pop_connect(pop, NULL, &error) == TRUE, "connect: success");
+       sput_fail_unless(net_client_pop_list(pop, &msg_list, TRUE, NULL) == TRUE, "LIST: success");
+       g_message("message count: %u", g_list_length(msg_list));
+       if (msg_list != NULL) {
+               sput_fail_unless(net_client_pop_retr(pop, msg_list, msg_cb, NULL, NULL) == TRUE, "retr ok");
+               sput_fail_unless(net_client_pop_dele(pop, NULL,NULL) == FALSE, "dele w/o message list");
+               sput_fail_unless(net_client_pop_dele(pop, msg_list, NULL) == TRUE, "dele ok");
+               g_list_free_full(msg_list, (GDestroyNotify) net_client_pop_msg_info_free);
+       }
+       op_res = net_client_pop_list(pop, &msg_list, TRUE, NULL);
+       sput_fail_unless((op_res == TRUE) && (msg_list == NULL), "LIST: success, empty");
+       g_object_unref(pop);
+
+}
+
 
 static void
 test_utils(void)


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]