/*
 * pop3.c:
 * implementation of rfc1939 POP3
 *
 * Copyright (c) 2000 Chris Lightfoot. All rights reserved.
 *
 */

static const char rcsid[] = "$Id: pop3.c,v 1.57 2003/11/06 01:19:27 chris Exp $";

#ifdef HAVE_CONFIG_H
#include "configuration.h"
#endif /* HAVE_CONFIG_H */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <syslog.h>
#include <time.h>
#include <unistd.h>
#include <netinet/in.h>    /* define struct sockaddr_in before arpa/inet.h's macros */
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/types.h>

#include "authswitch.h"
#include "connection.h"
#include "util.h"

extern int verbose;

int append_domain;  /* Do we automatically try user@domain if user alone fails to authenticate? */
int strip_domain;   /* Automatically try user if user@domain fails? */
int apop_only;      /* Disconnect any client which says USER, unless their
                       connection is secured. */
int log_bad_pass;   /* Log bad passwords for support purposes. */

/* do_capa CONNECTION COMMAND
 * CAPA command; list capabilities. */
static enum connection_action do_capa(connection c) {
    const char *capas[] = {
            "PIPELINING", "TOP", "USER", "UIDL",
#ifdef REVEAL_IMPLEMENTATION
            "IMPLEMENTATION tpop3d-" TPOP3D_VERSION,
#endif
#ifdef USE_TLS
            "STLS",
#endif
            NULL /* last element must be NULL */
        }, **p;

    connection_sendresponse(c, 1, _("Capability list follows"));
    for (p = capas; *p; ++p) {
        if (strcmp(*p, "USER") == 0 && (apop_only && !c->secured))
            continue;
        
#ifdef USE_TLS
        if (strcmp(*p, "STLS") == 0 && c->l->tls.mode != stls)
            continue;
#endif

        connection_sendline(c, *p);
    }

    connection_sendline(c, ".");
    return do_nothing;
}

/* do_user CONNECTION COMMAND
 * USER command; supply username of client. Returns 1 on success or 0 on
 * failure. */
static int do_user(connection c, const pop3command p) {
    if (p->toks->num != 2) {
        connection_sendresponse(c, 0, _("No, that's not right."));
        return 0;
    } else if (c->user) {
        connection_sendresponse(c, 0, _("But you already said `USER'."));
        return 0;
    } else {
        c->user = xstrdup((char*)p->toks->toks[1]);
        if (!c->user)
#ifndef NO_SNIDE_COMMENTS
            connection_sendresponse(c, 0, _("Tell me your name, knave!"));
#else
            connection_sendresponse(c, 0, _("USER command must be followed by a username."));
#endif
        return 1;
    }
}

/* do_pass CONNECTION COMMAND
 * PASS command; supply password of user. Returns 1 on success or 0 on
 * failure. */
static int do_pass(connection c, const pop3command p) {
    if (p->toks->num != 2) {
        connection_sendresponse(c, 0, _("No, that's not right."));
        return 0;
    } else if (c->pass) {
        connection_sendresponse(c, 0, _("But you already said `PASS'."));
        return 0;
    } else {
        c->pass = xstrdup(p->toks->toks[1]);
        if (!c->pass)
            connection_sendresponse(c, 0, _("You must give a password."));
        return 1;
    }
}

/* do_apop CONNECTION COMMAND
 * APOP command; supply MD5 authentication data. */
static enum connection_action do_apop(connection c, const pop3command p) {
    char *name, *hexdigest;
    unsigned char digest[16];

    ++c->n_auth_tries;

    if (p->toks->num != 3) {
        connection_sendresponse(c, 0, _("No, that's not right."));
        return do_nothing;
    }

    name =      (char*)p->toks->toks[1];
    hexdigest = (char*)p->toks->toks[2];

    if (c->n_auth_tries == MAX_AUTH_TRIES) {
#ifndef NO_SNIDE_COMMENTS
        connection_sendresponse(c, 0, _("This is ridiculous. I give up."));
#else
        connection_sendresponse(c, 0, _("Too many authentication attempts."));
#endif
        return close_connection;
    }

    if (!name || *name == 0) {
        connection_sendresponse(c, 0, _("That's not right."));
        return do_nothing;
    }

    if (strlen(hexdigest) != 32) {
#ifndef NO_SNIDE_COMMENTS
        connection_sendresponse(c, 0, _("Try again, but get it right next time."));
#else
        connection_sendresponse(c, 0, _("Authentication string is invalid."));
#endif
        return do_nothing;
    }

    /* Obtain digest */
    if (!unhex_digest(hexdigest, digest)) {
#ifndef NO_SNIDE_COMMENTS
        connection_sendresponse(c, 0, _("Clueless bunny!"));
#else
        connection_sendresponse(c, 0, _("Authentication failed."));
#endif
        return do_nothing;
    }

    c->a = authcontext_new_apop(name, NULL, c->domain, c->timestamp, digest, c->remote_ip, c->local_ip);

    /* Maybe retry authentication with an added or removed domain name. */
    if (!c->a && (strip_domain || append_domain)) {
        int n, len;
        len = strlen(name);
        n = strcspn(name, DOMAIN_SEPARATORS);
        if (append_domain && c->domain && n == len)
            /* OK, if we have a domain name, try appending that. */
            c->a = authcontext_new_apop(name, name, c->domain, c->timestamp, digest, c->remote_ip, c->local_ip);
        else if (strip_domain && n != len) {
            /* Try stripping off the supplied domain name. */
            char *u;
            u = xstrdup(name);
            u[n] = 0;
            c->a = authcontext_new_apop(u, NULL, NULL, c->timestamp, digest, c->remote_ip, c->local_ip);
            xfree(u);
        }
    }

    if (c->a) {
        /* Now save a new ID string for this client. */
        xfree(c->idstr);
        c->idstr =xmalloc(strlen(c->a->user) + 2 + strlen(inet_ntoa(c->sin.sin_addr)) + 16);
        sprintf(c->idstr, "[%d]%s(%s)", c->s, c->a->user, inet_ntoa(c->sin.sin_addr));

        c->state = transaction;
        return fork_and_setuid;
    } else {
        /* Authentication failed. */
        connection_freeze(c);
        if (c->n_auth_tries == MAX_AUTH_TRIES) {
#ifndef NO_SNIDE_COMMENTS
            connection_sendresponse(c, 0, _("This is ridiculous. I give up."));
#else
            connection_sendresponse(c, 0, _("Too many authentication attempts."));
#endif

            log_print(LOG_ERR, _("connection_do: client `%s': username `%s': failed to log in after %d attempts"), c->idstr, name, MAX_AUTH_TRIES);
            return close_connection;
        } else {
#ifndef NO_SNIDE_COMMENTS
            connection_sendresponse(c, 0, _("Lies! Try again!"));
#else
            connection_sendresponse(c, 0, _("Authentication failed."));
#endif
            log_print(LOG_ERR, _("connection_do: client `%s': username `%s': %d authentication failures"), c->idstr, name, c->n_auth_tries);
            return do_nothing;
        }
    }
}

/* do_list CONNECTION MSGNUM
 * LIST command; MSGNUM is the argument or -1 if none was specified. */
void do_list(connection c, const int msg_num) {
    /* Gives exact sizes taking account of the "From " lines. */
    if (msg_num != -1) {
        struct indexpoint *curmsg;
        curmsg = c->m->index + msg_num;
        if (curmsg->deleted)
            connection_sendresponse(c, 0, _("That message is no more."));
        else {
            char response[32] = {0};
            snprintf(response, 31, "%d %d", 1 + msg_num, (int)(curmsg->msglength - curmsg->length - 1));
            connection_sendresponse(c, 1, response);
        }
    } else {
        struct indexpoint *m;
        int nn = 0;
        if (!(connection_sendresponse(c, 1, _("Scan list follows:"))))
            return;
        for (m = c->m->index; m < c->m->index + c->m->num; ++m) {
            if (!m->deleted) {
                char response[32] = {0};
                snprintf(response, 31, "%d %d", 1 + m - c->m->index, (int)(m->msglength - m->length - 1));
                if (!(connection_sendline(c, response)))
                    return;
                ++nn;
            }
        }
        connection_sendline(c, ".");
        /* That might have taken a long time. */
        c->idlesince = time(NULL);
        if (verbose)
            log_print(LOG_DEBUG, _("do_list: client %s: sent %d-line scan list"), c->idstr, nn + 1);
    }
}

/* do_uidl CONNECTION MSGNUM
 * UIDL command: MSGNUM is the argument or -1 if none was specified. */
static void do_uidl(connection c, const int msg_num) {
    /* It isn't guaranteed that these IDs are unique; it is likely, though.
     * See RFC1939. */
    if (msg_num != -1) {
        struct indexpoint *curmsg;
        curmsg = c->m->index + msg_num;
        if (curmsg->deleted)
            connection_sendresponse(c, 0, _("That message is no more."));
        else {
            char response[64] = {0};
            snprintf(response, 63, "%d %s", 1 + msg_num, hex_digest(curmsg->hash));
            connection_sendresponse(c, 1, response);
        }
    } else {
        struct indexpoint *m;
        int nn = 0;
        if (!(connection_sendresponse(c, 1, _("ID list follows:"))))
            return;
        for (m = c->m->index; m < c->m->index + c->m->num; ++m) {
            if (!m->deleted) {
                char response[64] = {0};
                snprintf(response, 63, "%d %s", 1 + m - c->m->index, hex_digest(m->hash));
                if (!connection_sendline(c, response))
                    return;
                ++nn;
            }
        }
        connection_sendline(c, ".");
        /* That might have taken a long time. */
        c->idlesince = time(NULL);
        if (verbose)
            log_print(LOG_DEBUG, _("do_uidl: client %s: sent %d-line unique ID list"), c->idstr, nn + 1);
        return;
    }
}

/* do_retr CONNECTION MSGNUM
 * RETR command; send whole of message MSGNUM. */
static enum connection_action do_retr(connection c, const int msg_num) {
    if (msg_num != -1) {
        struct indexpoint *curmsg;
        curmsg = c->m->index + msg_num;
        if (curmsg->deleted)
            connection_sendresponse(c, 0, _("That message is no more."));
        else {
            int n;
            
            if (verbose)
                log_print(LOG_DEBUG, _("do_retr: client %s: sending message %d (%d bytes)"),
                        c->idstr, msg_num + 1, (int)curmsg->msglength);
        
            if ((n = c->m->sendmessage(c->m, c, msg_num, -1)) == -2)
                return close_connection;
            
            /* That might have taken a long time. */
            c->idlesince = time(NULL);
            if (verbose) {
                if (n >= 0)
                    log_print(LOG_DEBUG, _("do_retr: client %s: sent message %d"), c->idstr, msg_num + 1);
                else
                    log_print(LOG_DEBUG, _("do_retr: client %s: failed to send message %d"), c->idstr, msg_num + 1);
            }
        }
    } else
        connection_sendresponse(c, 0, _("Which message do you want to see?"));
    return do_nothing;
}

/* do_top CONNECTION MSGNUM NUM
 * TOP command; send headers and first NUM lines of message MSGNUM. */
static enum connection_action do_top(connection c, const int msg_num, const int nlines) {
    struct indexpoint *curmsg;
    curmsg = c->m->index + msg_num;

    if (msg_num == -1)
        connection_sendresponse(c, 0, _("What do you want to see?"));
    else if (nlines == -1)
        connection_sendresponse(c, 0, _("But how much do you want to see?"));
    else if (curmsg->deleted)
        connection_sendresponse(c, 0, _("That message is no more."));
    else {
        int n;
        
        if (verbose)
            log_print(LOG_DEBUG, _("do_top: client %s: sending headers and up to %d lines of message %d (< %d bytes)"),
                    c->idstr, nlines, msg_num + 1, (int)curmsg->msglength);
        
        if ((n = c->m->sendmessage(c->m, c, msg_num, nlines)) == -2)
            return close_connection;

        /* That might have taken a long time. */
        c->idlesince = time(NULL);
        if (verbose) {
            if (n >= 0)
                log_print(LOG_DEBUG, _("do_top: client %s: sent headers and up to %d lines of message %d"), c->idstr, nlines, msg_num + 1);
            else
                log_print(LOG_DEBUG, _("do_top: client %s: failed to send message %d"), c->idstr, msg_num + 1);
        }
    }
    
    return do_nothing;
}

/* do_dele
 * DELE command; delete message. */
void do_dele(connection c, const int msg_num) {
    struct indexpoint *curmsg;
    curmsg = c->m->index + msg_num;

    if (msg_num == -1)
        connection_sendresponse(c, 0, _("But which message do you want to delete?"));
    else {
        curmsg->deleted = 1;
        ++c->m->numdeleted;
        c->m->sizedeleted += curmsg->msglength;
        connection_sendresponse(c, 1, _("Done."));
    }
}

/* connection_do CONNECTION COMMAND
 * Have CONNECTION perforem COMMAND, returning a code indicating what the
 * caller should do. */
enum connection_action connection_do(connection c, const pop3command p) {
    /* This breaks the RFC, but is sensible. */
    if (p->cmd != NOOP && p->cmd != UNKNOWN) c->idlesince = time(NULL);

    if (c->state == authorisation) {
        /* Authorisation state: gather username and password or whatever. */
        switch (p->cmd) {
            case CAPA:
                return do_capa(c);
            
            case USER:
                if (apop_only && !c->secured) {
                    connection_sendresponse(c, 0, _("Sorry, you must use APOP"));
                    return close_connection;
                } else if (!do_user(c, p))
                    return do_nothing;
                break;

            case PASS:
                if (apop_only && !c->secured) {
                    connection_sendresponse(c, 0, _("Sorry, you must use APOP"));
                    return close_connection;
                } else if (!do_pass(c, p))
                    return do_nothing;
                break;

            case APOP:
                return do_apop(c, p);

            case QUIT:
#ifndef NO_SNIDE_COMMENTS
                connection_sendresponse(c, 1, _("Fine. Be that way."));
#else
                connection_sendresponse(c, 1, _("Done."));
#endif
                return close_connection;

            case STLS:
#ifdef USE_TLS
                if (c->secured) {
                    connection_sendresponse(c, 0, _("You're already using TLS"));
                    return do_nothing;
                } else if (c->l->tls.mode == stls) {
                    struct ioabs_tls *newio;
                    if (!(connection_sendresponse(c, 1, _("Begin TLS negotiation"))))
                        return close_connection;
                    if ((newio = ioabs_tls_create(c, c->l))) {
                        log_print(LOG_INFO, _("connection_do: client %s: negotiating TLS connection"), c->idstr);
                        c->io->destroy(c);
                        c->io = (struct ioabs*)newio;
                        return do_nothing;
                    } else {
                        /* Otherwise we've issued an OK response but haven't
                         * managed to negotiate a TLS connection. */
                        log_print(LOG_INFO, _("connection_do: client %s: tried and failed to negotiate TLS connection; closing connection"), c->idstr);
                        return close_connection;
                    }
                }
#endif
                log_print(LOG_INFO, _("connection_do: client %s: asked for TLS, not available"), c->idstr);
                connection_sendresponse(c, 0, _("Sorry, TLS not available"));
                return do_nothing;

            case UNKNOWN:
#ifndef NO_SNIDE_COMMENTS
                connection_sendresponse(c, 0, _("Do you actually know how to use this thing?"));
#else
                connection_sendresponse(c, 0, _("The command sent is invalid or unimplemented."));
#endif
                return do_nothing;

            default:
                connection_sendresponse(c, 0, _("Not now. First log in."));
                return do_nothing;
        }

        /* Do we now have enough information to authenticate using USER/PASS? */
        if (!c->a && c->user && c->pass) {
            c->a = authcontext_new_user_pass(c->user, NULL, c->domain, c->pass, c->remote_ip, c->local_ip);
            
            /* Maybe retry authentication with an added or removed domain name. */
            if (!c->a && (append_domain || strip_domain)) {
                int n, len;
                len = strlen(c->user);
                n = strcspn(c->user, DOMAIN_SEPARATORS);
                if (append_domain && c->domain && n == len)
                    /* OK, if we have a domain name, try appending that. */
                    c->a = authcontext_new_user_pass(c->user, c->user, c->domain, c->pass, c->remote_ip, c->local_ip);
                else if (strip_domain && n != len) {
                    /* Try stripping off the supplied domain name. */
                    char *u;
                    u = xstrdup(c->user);
                    u[n] = 0;
                    c->a = authcontext_new_user_pass(u, NULL, NULL, c->pass, c->remote_ip, c->local_ip);
                    xfree(u);
                }
            }

            if (c->a) {
                /* Now save a new ID string for this client. */
                xfree(c->idstr);
                c->idstr =xmalloc(strlen(c->a->user) + 2 + strlen(inet_ntoa(c->sin.sin_addr)) + 16);
                sprintf(c->idstr, "[%d]%s(%s)", c->s, c->a->user, inet_ntoa(c->sin.sin_addr));

                memset(c->pass, 0, strlen(c->pass));
                c->state = transaction;
                return fork_and_setuid; /* Code in main.c sends response in case of error. */
            } else {
                enum connection_action act;

                /*
                 * It is useful for ISPs to be able to log failing passwords
                 * sent by misconfigured clients. This is an invasion of
                 * privacy, but there we go.
                 */
                if (log_bad_pass)
                    log_print(LOG_INFO, _("connection_do: client `%s': username `%s': failing password is `%s'"), c->idstr, c->user, c->pass);
                            
                connection_freeze(c);
                ++c->n_auth_tries;
                if (c->n_auth_tries == MAX_AUTH_TRIES) {
#ifndef NO_SNIDE_COMMENTS
                    connection_sendresponse(c, 0, _("This is ridiculous. I give up."));
#else
                    connection_sendresponse(c, 0, _("Too many authentication attempts."));
#endif
                    log_print(LOG_ERR, _("connection_do: client `%s': username `%s': failed to log in after %d attempts"), c->idstr, c->user, MAX_AUTH_TRIES);
                    act = close_connection;
                } else {
#ifndef NO_SNIDE_COMMENTS
                    connection_sendresponse(c, 0, _("Lies! Try again!"));
#else
                    connection_sendresponse(c, 0, _("Authentication failed."));
#endif
                    log_print(LOG_ERR, _("connection_do: client `%s': username `%s': %d authentication failures"), c->idstr, c->user, c->n_auth_tries);
                    act = do_nothing;
                }

                memset(c->pass, 0, strlen(c->pass));
                xfree(c->pass);
                c->pass = NULL;

                xfree(c->user);
                c->user = NULL;
                
                return act;
            }
        } else {
            connection_sendresponse(c, 1, c->pass ? _("What's your name?") : _("Tell me your password."));
            return do_nothing;
        }
    } else if (c->state == transaction) { 
        /* Transaction state: do things to mailbox. */
        char *a = NULL;
        int num_args, msg_num = -1, nlines = -1;
        struct indexpoint *i, *curmsg;
        mailbox curmbox;
        char response[32] = {0};

        curmbox = c->m; /* this connection's mailbox */
        num_args = p->toks->num - 1;

        /* No command has more than two arguments. */
        if (num_args > 2) {
#ifndef NO_SNIDE_COMMENTS
            connection_sendresponse(c, 0, _("Already, you have told me too much."));
#else
            connection_sendresponse(c, 0, _("Too many arguments for command."));
#endif
            return do_nothing;
        }
        
        /* The first argument, if any, is always interpreted as a message
         * number. */
        if (num_args >= 1) {
            a = p->toks->toks[1];
            if (a && strlen(a) > 0) {
                char *b;
                msg_num = strtol(a, &b, 10);
                --msg_num; /* RFC1939 demands that mailspools be indexed from 1 */
                if (!(b && !*b && b != a && msg_num >= 0 && msg_num < curmbox->num)) {
#ifndef NO_SNIDE_COMMENTS
                    connection_sendresponse(c, 0, _("That does not compute."));
#else
                    connection_sendresponse(c, 0, _("Command argument should be numeric."));
#endif
                    return do_nothing;
                }
                curmsg = c->m->index + msg_num;
            }
        }

        /* The second argument is only ever used with TOP and is a number of
         * lines. */
        if (num_args == 2) {
            if (p->cmd == TOP) {
                a = p->toks->toks[2];
                if (a && strlen(a) > 0) {
                    char *b;
                    nlines = strtol(a, &b, 10);
                    if (!(b && !*b && b != a && nlines >= 0)) {
#ifndef NO_SNIDE_COMMENTS
                        connection_sendresponse(c, 0, _("Can you actually count?"));
#else
                        connection_sendresponse(c, 0, _("Command argument should be numeric."));
#endif
                        return do_nothing;
                    }
                }
            } else {
                connection_sendresponse(c, 0, _("Nope, that doesn't sound right at all."));
                return do_nothing;
            }
        }
        
        switch (p->cmd) {
            case LIST:
                do_list(c, msg_num);
                break;

            case UIDL:
                do_uidl(c, msg_num);
                break;

            case DELE:
                do_dele(c, msg_num);
                break;

            case RETR:
                return do_retr(c, msg_num);

            case TOP:
                return do_top(c, msg_num, nlines);

            case STAT:
                snprintf(response, 31, "%d %d", curmbox->num - curmbox->numdeleted, curmbox->totalsize - curmbox->sizedeleted);
                connection_sendresponse(c, 1, response);
                break;

            case RSET:
                for (i = curmbox->index; i < curmbox->index + curmbox->num; ++i) i->deleted = 0;
                curmbox->numdeleted = 0;
                curmbox->sizedeleted = 0;
                connection_sendresponse(c, 1, _("Done."));
                break;

            case QUIT:
                /* Now perform UPDATE */
                if ((curmbox)->apply_changes(curmbox)) connection_sendresponse(c, 1, _("Done"));
                else connection_sendresponse(c, 0, _("Something went wrong."));
                return close_connection;

            case NOOP:
                connection_sendresponse(c, 1, _("I'm still here."));
                break;

            case LAST:
                connection_sendresponse(c, 0, _("Sorry, the LAST command was removed in RFC1725."));
                break;

            case STLS:
#ifndef NO_SNIDE_COMMENTS
                connection_sendresponse(c, 0, _("It's a bit late for that now, isn't it?"));
#else
                connection_sendresponse(c, 0, _("STLS command available only in AUTHORIZATION stat"));
#endif
                break;

            default:
#ifndef NO_SNIDE_COMMENTS
                connection_sendresponse(c, 0, _("Do you actually know how to use this thing?"));
#else
                connection_sendresponse(c, 0, _("The command sent was invalid or unimplemented."));
#endif
                break;
        }

        return do_nothing;
    } else {
        /* Can't happen, but keep the compiler quiet... */
        connection_sendresponse(c, 0, _("connection_do: unknown state, closing connection."));
        return close_connection;
    }
}

/* connection_start_transaction CONNECTION
 * Put CONNECTION in the `transaction' state, opening the mailbox. Returns 1
 * on success or 0 on failure. */
int connection_start_transaction(connection c) {
    if (!c) return 0;
    
    if (c->a->gid != getgid()) {
        log_print(LOG_ERR, _("connection_start_transaction: wrong gid"));
        return 0;
    }
    if (c->a->uid != getuid()) {
        log_print(LOG_ERR, _("connection_start_transaction: wrong uid"));
        return 0;
    }
    
    if (c->a->mailbox) {
        c->m = mailbox_new(c->a->mailbox, c->a->mboxdrv);
        if (c->m == MBOX_NOENT) c->m = emptymbox_new(NULL);
    } else
        c->m = find_mailbox(c->a);

    if (!c->m) return 0;
    else return 1;
}


syntax highlighted by Code2HTML, v. 0.9.1