/* $Id: client.c,v 1.691 2011/06/28 00:13:48 sbajic Exp $ */

/*
 DSPAM
 COPYRIGHT (C) 2002-2012 DSPAM PROJECT

 This program is free software: you can redistribute it and/or modify
 it under the terms of the GNU Affero General Public License as
 published by the Free Software Foundation, either version 3 of the
 License, or (at your option) any later version.

 This program 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 Affero General Public License for more details.

 You should have received a copy of the GNU Affero General Public License
 along with this program.  If not, see <http://www.gnu.org/licenses/>.

*/

/*
 * client.c - client-based functions (for operating in client/daemon mode)c
 *
 * DESCRIPTION
 *   Client-based functions are called when --client is specified on the
 *   commandline or by dspamc (where --client is inferred). The client
 *   functions connect to a DSPAM server for processing (rather than the
 *   usual behavior which is to process the message itself). Client
 *   functions are also used when delivering via LMTP or SMTP.
 */

#ifdef HAVE_CONFIG_H
#include <auto-config.h>
#endif

#ifdef DAEMON

#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include <errno.h>
#include <error.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/un.h>
#include <sys/socket.h>
#include <netdb.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <arpa/inet.h>
#ifndef _WIN32
#include <unistd.h>
#include <pwd.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <sysexits.h>

#include "client.h"
#include "dspam.h"
#include "config.h"
#include "util.h"
#include "language.h"
#include "buffer.h"

/*
 * client_process(AGENT_CTX *, buffer *)
 *
 * DESCRIPTION
 *   connect to a dspam daemon socket and attempt to process a message
 *   this function is called by the dspam agent when --client is specified
 *
 * INPUT ARGUMENTS
 *      ATX     	agent context
 *      message		message to be processed
 *
 * RETURN VALUES
 *   returns 0 on success
*/

int client_process(AGENT_CTX *ATX, buffer *message) {
  char buf[1024], err[256];
  struct nt_node *node_nt;
  struct nt_c c_nt;
  int exitcode = 0, msglen;
  THREAD_CTX TTX;
  int i;

  TTX.sockfd = client_connect(ATX, 0);
  if (TTX.sockfd <0) {
    LOG(LOG_WARNING, ERR_CLIENT_CONNECT);
    STATUS(ERR_CLIENT_CONNECT);
    return TTX.sockfd;
  }

  TTX.packet_buffer = buffer_create(NULL);
  if (TTX.packet_buffer == NULL) 
    goto BAIL;

  /* LHLO / MAIL FROM - Authenticate on the server */

  if (client_authenticate(&TTX, ATX->client_args)<0) {
    LOG(LOG_WARNING, ERR_CLIENT_AUTH_FAILED);
    STATUS(ERR_CLIENT_AUTH_FAILED);
    goto QUIT;
  }

  /* RCPT TO - Send recipient information */

  strcpy(buf, "RCPT TO: ");
  node_nt = c_nt_first(ATX->users, &c_nt);
  while(node_nt != NULL) {
    const char *ptr = (const char *) node_nt->ptr;
    snprintf(buf, sizeof(buf), "RCPT TO: <%s>", ptr);
    if (send_socket(&TTX, buf)<=0) {
      STATUS(ERR_CLIENT_SEND_FAILED);
      goto BAIL;
    }

    if (client_getcode(&TTX, err, sizeof(err))!=LMTP_OK) {
      STATUS("%s", err);
      goto QUIT;
    }

    node_nt = c_nt_next(ATX->users, &c_nt);
  }

  /* DATA - Send message */

  if (send_socket(&TTX, "DATA")<=0) 
    goto BAIL;

  if (client_getcode(&TTX, err, sizeof(err))!=LMTP_DATA) {
    STATUS("%s", err);
    goto QUIT;
  }

  i = 0;
  msglen = strlen(message->data);
  while(i<msglen) {
    int r;
    int t;
    int buflen;

    /*
     * fill buf with partial msg, replacing \n with \r\n
     * and do dot stuffing, if needed.
     */
    buflen = 0;
    while ((size_t)buflen < (sizeof(buf) - 4) && i < msglen) {
      if (i > 0) {
        if (message->data[i] == '\n') {
          /* only replace \n and not \r\n */
          if (message->data[i - 1] != '\r') {
            buf[buflen] = '\r';
            buflen++;
          }
          /* take care of dot stuffing \n */
          if (message->data[i + 1] && message->data[i + 1] == '.') {
            buf[buflen] = '\n';
            buflen++;
            buf[buflen] = '.';
            buflen++;
            buf[buflen] = '.';
            buflen++;
            i += 2;
            continue;
          }
        }
      }

      buf[buflen] = message->data[i];
      buflen++;
      i++;
    }

    /* send buf */
    t = 0;
    while (t < buflen) {
      r = send(TTX.sockfd, buf+t, buflen - t, 0);
      if (r <= 0) {
        LOG(LOG_ERR, ERR_CLIENT_SEND_FAILED);
        STATUS(ERR_CLIENT_SEND_FAILED);
        goto BAIL;
      }
      t += r;
    }
  }

  if (message->data[msglen-1]!= '\n') {
    if (send_socket(&TTX, "")<=0) {
     STATUS(ERR_CLIENT_SEND_FAILED);
     goto BAIL;
    }
  }

  if (send_socket(&TTX, ".")<=0) {
    STATUS(ERR_CLIENT_SEND_FAILED);
    goto BAIL;
  }

  /* Server response */

  if (ATX->flags & DAF_STDOUT || ATX->flags & DAF_SUMMARY ||
      ATX->operating_mode == DSM_CLASSIFY) 
   {
    char *line = NULL;
    int head = !(ATX->flags & DAF_STDOUT);

    if (ATX->flags & DAF_SUMMARY)
      head = 1;

    line = client_getline(&TTX, 300);

    while(line != NULL && strcmp(line, ".")) {
      chomp(line);
      if (!head) {
        head = 1;
        if (!strncmp(line, "250 ", 4)) {
          free(line);
          goto QUIT;
        }

        if (!strcmp(line, "X-Daemon-Classification: SPAM") && 
            _ds_match_attribute(agent_config, "Broken", "returnCodes")) 
        {
          exitcode = 99;
        }
      } else {
        printf("%s\n", line);
      } 
      free(line);
      line = client_getline(&TTX, 300);
      if (line) chomp(line);
    }
    free(line);
  } else {
    for(i=0;i<ATX->users->items;i++) {
      char *input = client_getline(&TTX, 300);
      char *x;
      int code = 500;

      if (!input) {
        goto BAIL;
      }
      x = strtok(input, " ");
      if (x) {
        code = atoi(x);
        if (code != LMTP_OK) {
          if (exitcode > 0) 
            exitcode = 0;
          exitcode--;
        } else {
          if (_ds_match_attribute(agent_config, "Broken", "returnCodes")) {
            x = strtok(NULL, ":");
            if (x)
              x = strtok(NULL, ":");
            if (x && strstr(x, "SPAM") && exitcode == 0)
              exitcode = 99;
          }
        }
      }
    }
  }

  send_socket(&TTX, "QUIT");
  client_getcode(&TTX, err, sizeof(err));
  close(TTX.sockfd);
  buffer_destroy(TTX.packet_buffer);
  return exitcode;

QUIT:
  send_socket(&TTX, "QUIT");
  client_getcode(&TTX, err, sizeof(err));

BAIL:
  exitcode = EFAILURE;
  buffer_destroy(TTX.packet_buffer);
  close(TTX.sockfd);
  return exitcode;
}

/*
 * client_connect(AGENT_CTX ATX, int flags)
 *
 * DESCRIPTION
 *   establish a connection to a server
 *
 * INPUT ARGUMENTS
 *      ATX             agent context
 *      flags		connection flags
 *
 * FLAGS
 *    CCF_PROCESS     Use ClientHost as destination
 *    CCF_DELIVERY    Use DeliveryHost as destination
 *
 *  RETURN VALUES
 *    returns 0 on success
 */

int client_connect(AGENT_CTX *ATX, int flags) {
  struct sockaddr_in addr;
  struct sockaddr_un saun;
  int sockfd;
  int yes = 1;
  int port = 24;
  int domain = 0;
  int addr_len;
  char *host;

  if (flags & CCF_DELIVERY) {
    host = _ds_read_attribute(agent_config, "DeliveryHost");

    if (_ds_read_attribute(agent_config, "DeliveryPort"))
      port = atoi(_ds_read_attribute(agent_config, "DeliveryPort"));

    if (ATX->recipient && ATX->recipient[0]) {
      char *domain = strchr(ATX->recipient, '@');
      if (domain) {
        char key[128];
        char lcdomain[strlen(ATX->recipient)];
        lc(lcdomain, domain+1);
        snprintf(key, sizeof(key), "DeliveryHost.%s", lcdomain);
        if (_ds_read_attribute(agent_config, key))
          host = _ds_read_attribute(agent_config, key);
        snprintf(key, sizeof(key), "DeliveryPort.%s", lcdomain);
        if (_ds_read_attribute(agent_config, key))
          port = atoi(_ds_read_attribute(agent_config, key));
      }
    }

    if (host && host[0] == '/') 
      domain = 1;

  } else {
    host = _ds_read_attribute(agent_config, "ClientHost");

    if (_ds_read_attribute(agent_config, "ClientPort"))
      port = atoi(_ds_read_attribute(agent_config, "ClientPort"));

    if (host && host[0] == '/')
      domain = 1;
  }

  if (host == NULL) {
    LOG(LOG_CRIT, ERR_CLIENT_INVALID_CONFIG);
    STATUS(ERR_CLIENT_INVALID_CONFIG);
    return EINVAL;
  }

  /* Connect (domain socket) */

  if (domain) {
    sockfd = socket(AF_UNIX, SOCK_STREAM, 0);
    saun.sun_family = AF_UNIX;
    strcpy(saun.sun_path, host);
    addr_len = sizeof(saun.sun_family) + strlen(saun.sun_path) + 1;

    LOGDEBUG(INFO_CLIENT_CONNECTING, host, 0);
    if(connect(sockfd, (struct sockaddr *)&saun, addr_len)<0) {
      LOG(LOG_ERR, ERR_CLIENT_CONNECT_SOCKET, host, strerror(errno));
      STATUS("%s", strerror(errno));
      close(sockfd);
      return EFAILURE;
    }

  /* Connect (TCP socket) */

  } else {
    sockfd = socket(AF_INET, SOCK_STREAM, 0);
    memset(&addr, 0, sizeof(struct sockaddr_in));
    addr.sin_family = AF_INET;
    addr.sin_addr.s_addr = inet_addr(host);
    addr.sin_port = htons(port);
    addr_len = sizeof(struct sockaddr_in);
    LOGDEBUG(INFO_CLIENT_CONNECTING, host, port);
    if(connect(sockfd, (struct sockaddr *)&addr, addr_len)<0) {
      LOG(LOG_ERR, ERR_CLIENT_CONNECT_HOST, host, port, strerror(errno));
      STATUS("%s", strerror(errno));
      close(sockfd);
      return EFAILURE;
    }
  }

  LOGDEBUG(INFO_CLIENT_CONNECTED);
  setsockopt(sockfd,SOL_SOCKET,TCP_NODELAY,&yes,sizeof(int));

  return sockfd;
}

/*
 * client_authenticate(AGENT_CTX *ATX, const char *mode)
 *
 * DESCRIPTION
 *   greet and authenticate on a server
 *
 * INPUT ARGUMENTS
 *      ATX             agent context
 *      mode		processing mode
 *
 * NOTES
 *   the process mode is passed using the DSPAMPROCESSMODE service tag
 *
 * RETURN VALUES
 *   returns 0 on success
 */

int client_authenticate(THREAD_CTX *TTX, const char *mode) {
  char *ident = _ds_read_attribute(agent_config, "ClientIdent");
  char buf[1024], err[128];
  char *ptr;
  char pmode[1024];

  pmode[0] = 0;
  if (mode) {
    int pos = 0, cpos = 0;
    for(;mode[cpos]&&(size_t)pos<(sizeof(pmode)-1);cpos++) {
      if (mode[cpos] == '"') {
        pmode[pos] = '\\';
        pos++;
      }
      pmode[pos] = mode[cpos];
      pos++;
    }
    pmode[pos] = 0;
  }

  if (!ident || !strchr(ident, '@')) {
    LOG(LOG_ERR, ERR_CLIENT_IDENT);
    return EINVAL;
  }

  ptr = client_expect(TTX, LMTP_GREETING, err, sizeof(err));
  if (ptr == NULL) {
    LOG(LOG_ERR, ERR_CLIENT_WHILE_AUTH, err);
    return EFAILURE;
  }
  free(ptr);

  snprintf(buf, sizeof(buf), "LHLO %s", strchr(ident, '@')+1);
  if (send_socket(TTX, buf)<=0) 
    return EFAILURE;

  if (client_getcode(TTX, err, sizeof(err))!=LMTP_OK) {
    return EFAILURE;
  }

  if (mode) {
    snprintf(buf, sizeof(buf), "MAIL FROM: <%s> DSPAMPROCESSMODE=\"%s\"", ident, pmode);
  } else {
    snprintf(buf, sizeof(buf), "MAIL FROM: <%s>", ident);
  }
  if (send_socket(TTX, buf)<=0) {
    return EFAILURE;
  }

  if (client_getcode(TTX, err, sizeof(err))!=LMTP_OK) {
    LOG(LOG_ERR, ERR_CLIENT_AUTHENTICATE);
    return EFAILURE;
  }

  return 0;
}

/*
 * client_expect(THREAD_CTX *TTX, int code, char *err, size_t len)
 *
 * DESCRIPTION
 *   wait for the appropriate return code, then return
 *
 * INPUT ARGUMENTS
 *    ATX    agent context
 *    code   return code to wait for
 *    err    error buffer
 *    len    buffer len
 *
 * RETURN VALUES
 *   allocated pointer to acknowledgement line, NULL on error
 *   err buffer is populated on error
 */

char * client_expect(THREAD_CTX *TTX, int code, char *err, size_t len) {
  char *inp, *dup, *ptr, *ptrptr;
  int recv_code;

  inp = client_getline(TTX, 300);
  while(inp != NULL) {
    recv_code = 0;
    dup = strdup(inp);
    if (!dup) {
      free(inp);
      LOG(LOG_CRIT, ERR_MEM_ALLOC);
      strlcpy(err, ERR_MEM_ALLOC, len);
      return NULL;
    }
    if (strncmp(dup, "250-", 4)) {
      ptr = strtok_r(dup, " ", &ptrptr);
      if (ptr) 
        recv_code = atoi(ptr);
      free(dup);
      if (recv_code == code) {
        err[0] = 0;
        return inp;
      }
      LOG(LOG_WARNING, ERR_CLIENT_RESPONSE_CODE, code, inp);
    }
    
    strlcpy(err, inp, len);
    free(inp);
    inp = client_getline(TTX, 300);
  }

  return NULL;
}

/*
 * client_parsecode(const char *err)
 *
 * DESCRIPTION
 *   parse response code from plain text
 *
 * INPUT ARGUMENTS
 *      err    error message to parse
 *
 * RETURN VALUES
 *   integer value of response code
 */

int client_parsecode(char *error) {
  char code[4];
  code[3] = 0;
  strncpy(code, error, 3);
  return atoi(code);
}

/*
 * client_getcode(THREAD_CTX *TTX, char *err, size_t len)
 *
 * DESCRIPTION
 *   retrieve a line of input and return response code
 *
 * INPUT ARGUMENTS  
 *     TTX    thread context containing sockfd
 *     err    error buffer
 *     len    buffer len
 *
 * RETURN VALUES
 *   integer value of response code
 */

int client_getcode(THREAD_CTX *TTX, char *err, size_t len) {
  char *inp, *ptr, *ptrptr = NULL;
  int i;

  inp = client_getline(TTX, 300);
  if (!inp)
    return EFAILURE;

  while(inp && !strncmp(inp, "250-", 4)) {
    free(inp);
    inp = client_getline(TTX, 300);
  }

  strlcpy(err, inp, len);
  ptr = strtok_r(inp, " ", &ptrptr);
  if (ptr == NULL)
    return EFAILURE;
  i = atoi(ptr);
  free(inp);
  return i;
}

/*
 * client_getline(THREAD_CTX *TTX, int timeout)
 *
 * DESCRIPTION
 *   read a complete line from a socket
 *
 * INPUT ARGUMENTS  
 *      TTX        thread context containing sockfd
 *      timeout    timeout (in seconds) to wait for input
 *
 * RETURN VALUES
 *   allocated pointer to input
 */

char *client_getline(THREAD_CTX *TTX, int timeout) {
  struct timeval tv;
  long recv_len;
  char buf[1024];
  char *pop;
  fd_set fds;
  int i;

  pop = pop_buffer(TTX);
  while(!pop) {
    tv.tv_sec = timeout;
    tv.tv_usec = 0;
    FD_ZERO(&fds);
    FD_SET(TTX->sockfd, &fds);
    i = select(TTX->sockfd+1, &fds, NULL, NULL, &tv);
    if (i<=0)
      return NULL;

    recv_len = recv(TTX->sockfd, buf, sizeof(buf)-1, 0);
    buf[recv_len] = 0;
    if (recv_len == 0)
      return NULL;
    buffer_cat(TTX->packet_buffer, buf);
    pop = pop_buffer(TTX);
  }

#ifdef VERBOSE
  LOGDEBUG("RECV: %s", pop);
#endif

  return pop;
}

/*
 * pop_buffer (THREAD_CTX *TTX)
 *
 * DESCRIPTION
 *   pop a line off the packet buffer
 *
 * INPUT ARGUMENTS
 *      TTX     thread context containing the packet buffer
 * 
 * RETURN VALUES
 *   allocated pointer to line, NULL if complete line isn't available
 */

char *pop_buffer(THREAD_CTX *TTX) {
  char *buf, *eol;
  long len;

  if (!TTX || !TTX->packet_buffer || !TTX->packet_buffer->data)
    return NULL;

  eol = strchr(TTX->packet_buffer->data, 10);
  if (!eol)
    return NULL;
  len = (eol - TTX->packet_buffer->data) + 1;
  buf = calloc(1, len+1);
  if (!buf) {
    LOG(LOG_CRIT, ERR_MEM_ALLOC);
    return NULL;
  }
  memcpy(buf, TTX->packet_buffer->data, len);
  memmove(TTX->packet_buffer->data, eol+1, strlen(eol+1)+1);
  TTX->packet_buffer->used -= len;
  return buf;
}

/*
 * send_socket(THREAD_CTX *TTX, const char *text)
 *
 * DESCRIPTION
 *   send a line of text to a socket
 *
 * INPUT ARGUMENTS
 *      TTX     thread context containing sockfd
 *      text    text to send
 *
 * RETURN VALUES
 *   number of bytes sent
 */

int send_socket(THREAD_CTX *TTX, const char *text) {
  int i = 0, r, msglen;

#ifdef VERBOSE
  LOGDEBUG("SEND: %s", text);
#endif

  msglen = strlen(text);
  while(i<msglen) {
    r = send(TTX->sockfd, text+i, msglen-i, 0);
    if (r <= 0) {
      return r;
    }
    i += r;
  }

  r = send(TTX->sockfd, "\r\n", 2, 0);
  if (r > 0) {
    i += r;
  }
  return i;
}

/*
 * deliver_socket(AGENT_CTX *ATX, const char *msg, int proto)
 *
 * DESCRIPTION
 *   delivers message via LMTP or SMTP (instead of TrustedDeliveryAgent) 
 *
 *   If LMTP/SMTP delivery was specified in dspam.conf, this function will be 
 *   called by deliver_message(). This function connects to and delivers the 
 *   message using standard LMTP or SMTP. Depending on how DSPAM was originally 
 *   called, either the address supplied with the incoming RCPT TO or the 
 *   address supplied on the commandline with --rcpt-to  will be used. If 
 *   neither are present, the username will be used. 
 *
 * INPUT ARGUMENTS
 *     ATX      agent context
 *     msg      message to send
 *     proto    protocol to use
 *
 * PROTOCOLS
 *     DDP_LMTP    LMTP
 *     DDP_SMTP    SMTP
 *
 * RETURN VALUES
 *   returns 0 on success
 */

int deliver_socket(AGENT_CTX *ATX, const char *msg, int proto) {
  THREAD_CTX TTX;
  char buf[1024], err[256];
  char *ident = _ds_read_attribute(agent_config, "DeliveryIdent");
  int exitcode = EFAILURE;
  int msglen, code;
  int buflen;
  char *inp;
  int i;
  int size_extension = 0;

  err[0] = 0;

  TTX.sockfd = client_connect(ATX, CCF_DELIVERY);
  if (TTX.sockfd <0) {
    STATUS(ERR_CLIENT_CONNECT);
    LOG(LOG_ERR, ERR_CLIENT_CONNECT);
    return TTX.sockfd;
  }

  TTX.packet_buffer = buffer_create(NULL);
  if (TTX.packet_buffer == NULL) {
    LOG(LOG_CRIT, ERR_MEM_ALLOC);
    STATUS(ERR_MEM_ALLOC);
    goto BAIL;
  }

  inp = client_expect(&TTX, LMTP_GREETING, err, sizeof(err));
  if (inp == NULL) {
    LOG(LOG_ERR, ERR_CLIENT_ON_GREETING, err);
    STATUS("%s", err);
    goto BAIL;
  }
  free(inp);

  /* LHLO / HELO */

  snprintf(buf, sizeof(buf), "%s %s", (proto == DDP_LMTP) ? "LHLO" : "HELO",
           (ident) ? ident : "localhost");
  if (send_socket(&TTX, buf)<=0) {
    LOG(LOG_ERR, ERR_CLIENT_SEND_FAILED);
    STATUS(ERR_CLIENT_SEND_FAILED);
    goto BAIL;
  }

  /* Check for SIZE extension */

  if (proto == DDP_LMTP) {
    char *dup, *ptr, *ptrptr;
    inp = client_getline(&TTX, 300);
    while(inp != NULL) {
      code = 0;
      dup = strdup(inp);
      if (!dup) {
        free(inp);
        LOG(LOG_CRIT, ERR_MEM_ALLOC);
        LOG(LOG_ERR, ERR_CLIENT_INVALID_RESPONSE, "LHLO", ERR_MEM_ALLOC);
        STATUS("LHLO: %s", ERR_MEM_ALLOC);
        goto QUIT;
      }
      if (!strcmp(dup, "250-SIZE") || (!strncmp(dup, "250-SIZE", 8) && strlen(dup)>=8 && isspace(dup[8]))) {
        free(inp);
        free(dup);
        size_extension = 1;
        inp = client_expect(&TTX, LMTP_OK, err, sizeof(err));
        break;
      } else if (strncmp(dup, "250-", 4)) {
        ptr = strtok_r(dup, " ", &ptrptr);
        if (ptr)
          code = atoi(ptr);
        if (code == LMTP_OK) {
          ptr = strtok_r(NULL, " ", &ptrptr);
          if (ptr && !strcmp(ptr, "SIZE"))
            size_extension = 1;
        }
        free(dup);
        if (code == LMTP_OK) {
          err[0] = 0;
          break;
        }
        LOG(LOG_WARNING, ERR_CLIENT_RESPONSE_CODE, code, inp);
      }
      strlcpy(err, inp, sizeof(err));
      free(inp);
      inp = client_getline(&TTX, 300);
    }
  } else {
    inp = client_expect(&TTX, LMTP_OK, err, sizeof(err));
  }
  if (inp == NULL) {
    LOG(LOG_ERR, ERR_CLIENT_INVALID_RESPONSE,
      (proto == DDP_LMTP) ? "LHLO" : "HELO", err);
    STATUS("%s: %s", (proto == DDP_LMTP) ? "LHLO" : "HELO", err);
    goto QUIT;
  }
  free(inp);

  /* MAIL FROM */

  if (proto == DDP_LMTP && size_extension == 1) {
    snprintf(buf, sizeof(buf), "MAIL FROM:<%s> SIZE=%ld",
             ATX->mailfrom, (long) strlen(msg));
  } else {
    snprintf(buf, sizeof(buf), "MAIL FROM:<%s>", ATX->mailfrom);
  }

  if (send_socket(&TTX, buf)<=0) {
    LOG(LOG_ERR, ERR_CLIENT_SEND_FAILED);
    STATUS(ERR_CLIENT_SEND_FAILED);
    goto BAIL;
  }

  code = client_getcode(&TTX, err, sizeof(err));

  if (code!=LMTP_OK) {
    LOG(LOG_ERR, ERR_CLIENT_RESPONSE, code, "MAIL FROM", err);
    if (code >= 500) 
      exitcode = EINVAL;
    chomp(err);
    STATUS((code >= 500) ? "Fatal: %s" : "Deferred: %s", err);
    goto QUIT;
  }

  /* RCPT TO */

  snprintf(buf, sizeof(buf), "RCPT TO:<%s>", (ATX->recipient) ? ATX->recipient : "");
  if (send_socket(&TTX, buf)<=0) {
    LOG(LOG_ERR, ERR_CLIENT_SEND_FAILED);
    STATUS(ERR_CLIENT_SEND_FAILED);
    goto BAIL;
  }

  code = client_getcode(&TTX, err, sizeof(err));

  if (code!=LMTP_OK) {
    LOG(LOG_ERR, ERR_CLIENT_RESPONSE, code, "RCPT TO", err);
    if (code >= 500)
      exitcode = EINVAL;  
    chomp(err);
    STATUS((code >= 500) ? "Fatal: %s" : "Deferred: %s", err);
    goto QUIT;
  }

  /* DATA */

  if (send_socket(&TTX, "DATA")<=0) {
    LOG(LOG_ERR, ERR_CLIENT_SEND_FAILED);
    STATUS(ERR_CLIENT_SEND_FAILED);
    goto BAIL;
  }

  code = client_getcode(&TTX, err, sizeof(err));
  if (code!=LMTP_DATA) {
    LOG(LOG_ERR, ERR_CLIENT_RESPONSE, code, "DATA", err);
    if (code >= 500)
      exitcode = EINVAL;
    chomp(err);
    STATUS((code >= 500) ? "Fatal: %s" : "Deferred: %s", err);
    goto QUIT;
  }

  i = 0;
  msglen = strlen(msg);
  while(i<msglen) {
    int r;
    int t;

    /* fill buf with partial msg, replacing \n with \r\n */
    buflen = 0;
    while ((size_t)buflen < (sizeof(buf) - 2) && i < msglen) {
      /* only replace \n and not \r\n */
      if (i > 0 && msg[i] == '\n' && msg[i - 1] != '\r') {
        buf[buflen] = '\r';
        buflen++;
      }

      /* escape dot if first character on line */
      if (msg[i] == '.' && (i == 0 || msg[i - 1] == '\n')) {
        buf[buflen] = '.';
        buflen++;
      }

      buf[buflen] = msg[i];
      buflen++;
      i++;
    }

    /* send buf */
    t = 0;
    while (t < buflen) {
      r = send(TTX.sockfd, buf+t, buflen - t, 0);
      if (r <= 0) {
        LOG(LOG_ERR, ERR_CLIENT_SEND_FAILED);
        STATUS(ERR_CLIENT_SEND_FAILED);
        goto BAIL;
      }
      t += r;
    }
  }

  if (msg[strlen(msg)-1]!= '\n') {
    if (send_socket(&TTX, "")<=0) {
      LOG(LOG_ERR, ERR_CLIENT_SEND_FAILED);
      STATUS(ERR_CLIENT_SEND_FAILED);
      goto BAIL;
    }
  }

  if (send_socket(&TTX, "\r\n.")<=0) {
    LOG(LOG_ERR, ERR_CLIENT_SEND_FAILED);
    STATUS(ERR_CLIENT_SEND_FAILED);
    goto BAIL;
  }

  /* server response */

  code = client_getcode(&TTX, err, sizeof(err));
  if (code < 200 || code >= 300) {
    LOG(LOG_ERR, ERR_CLIENT_RESPONSE, code, "message data", err);
    if (code >= 400 && code < 500)
      exitcode = EX_TEMPFAIL;
    else if (code >= 500)
      exitcode = EINVAL;
    chomp(err);
    STATUS((code >= 500) ? "Fatal: %s" : "Deferred: %s", err);
    goto QUIT;
  }

  send_socket(&TTX, "QUIT");
  client_getcode(&TTX, err, sizeof(err));
  close(TTX.sockfd);
  buffer_destroy(TTX.packet_buffer);
  return 0;

QUIT:
  send_socket(&TTX, "QUIT");
  client_getcode(&TTX, err, sizeof(err));
  buffer_destroy(TTX.packet_buffer);
  close(TTX.sockfd);
  return exitcode;

BAIL:
  LOG(LOG_ERR, ERR_CLIENT_DELIVERY_FAILED);
  buffer_destroy(TTX.packet_buffer);
  close(TTX.sockfd);
  return exitcode;
}

#endif /* DAEMON */
