// nullmailer -- a simple relay-only MTA
// Copyright (C) 2018  Bruce Guenter <bruce@untroubled.org>
//
// This program is free software; you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 2 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
//
// You can contact me at <bruce@untroubled.org>.  There is also a mailing list
// available to discuss this package.  To subscribe, send an email to
// <nullmailer-subscribe@lists.untroubled.org>.

#include "config.h"
#include <stdlib.h>
#include <unistd.h>
#include "base64.h"
#include "connect.h"
#include "errcodes.h"
#include "fdbuf/fdbuf.h"
#include "itoa.h"
#include "mystring/mystring.h"
#include "protocol.h"

const int default_port = 25;
const int default_tls_port = 465;
const char* cli_program = "smtp";
const char* cli_help_prefix = "Send an email message via SMTP\n";

class smtp 
{
  fdibuf& in;
  fdobuf& out;
  mystring caps;
public:
  smtp(fdibuf& netin, fdobuf& netout);
  ~smtp();
  int get(mystring& str);
  int put(mystring cmd, mystring& result);
  void docmd(mystring cmd, int range, mystring& result, int permfail=ERR_MSG_PERMFAIL);
  void docmd(mystring cmd, int range, int permfail=ERR_MSG_PERMFAIL);
  void dohelo(bool ehlo);
  bool hascap(const char* name, const char* word = NULL);
  void auth_login(void);
  void auth_plain(void);
  void send_data(fdibuf& msg);
  void send_envelope(fdibuf& msg);
  void send(fdibuf& msg);
};

smtp::smtp(fdibuf& netin, fdobuf& netout)
  : in(netin), out(netout)
{
}

smtp::~smtp()
{
}

int smtp::get(mystring& str)
{
  mystring tmp;
  str = "";
  int code = -1;
  while(in.getline(tmp)) {
    if(tmp[tmp.length()-1] == '\r')
      tmp = tmp.left(tmp.length()-1);
    code = atoi(tmp.c_str());
    if(!!str)
      str += "\n";
    str += tmp;
    if(tmp[3] != '-')
      break;
  }
  return code;
}

int smtp::put(mystring cmd, mystring& result)
{
  out << cmd << "\r\n";
  if(!out.flush())
    return -1;
  return get(result);
}

void smtp::docmd(mystring cmd, int range, mystring& result, int permfail)
{
  int code;
  if(!cmd)
    code = get(result);
  else
    code = put(cmd, result);
  if(code < range || code >= (range+100)) {
    int e;
    if(code >= 500)
      e = permfail;
    else if(code >= 400)
      e = ERR_MSG_TEMPFAIL;
    else
      e = ERR_PROTO;
    out << "QUIT\r\n";
    out.flush();
    protocol_fail(e, result.c_str());
  }
}

void smtp::docmd(mystring cmd, int range, int permfail)
{
  mystring msg;
  docmd(cmd, range, msg, permfail);
}

void smtp::dohelo(bool ehlo)
{
  mystring hh = getenv("HELOHOST");
  if (!hh) protocol_fail(1, "$HELOHOST is not set");
  docmd((ehlo ? "EHLO " : "HELO ") + hh, 200, caps);
}

static int issep(char ch)
{
  return ch == ' ' || ch == '\n' || ch == '\0';
}

bool smtp::hascap(const char* name, const char* word)
{
  const size_t namelen = strlen(name);
  int i = -1;
  do {
    const char* s = caps.c_str() + i + 5;
    if (strncasecmp(s, name, namelen) == 0) {
      if (s[namelen] == '\n')
	return word == NULL;
      else if (s[namelen] == ' ') {
	if (word == NULL)
	  return true;
	s += namelen + 1;
	const size_t wordlen = strlen(word);
	do {
	  if (strncasecmp(s, word, wordlen) == 0 && issep(s[wordlen]))
	    return true;
	  while (!issep(*s))
	    ++s;
	} while (*s++ == ' ');
	return false;
      }
    }
    i = caps.find_first('\n', i+1);
  } while (i > 0);
  return false;
}

void smtp::auth_login(void)
{
  mystring encoded;
  base64_encode(user, encoded);
  docmd("AUTH LOGIN " + encoded, 300, ERR_AUTH_FAILED);
  encoded = "";
  base64_encode(pass, encoded);
  docmd(encoded, 200);
}

void smtp::auth_plain(void)
{
  mystring plain(user);
  plain += '\0';
  plain += user;
  plain += '\0';
  plain += pass;
  mystring encoded = "AUTH PLAIN ";
  base64_encode(plain, encoded);
  docmd(encoded, 200, ERR_AUTH_FAILED);
}

void smtp::send_envelope(fdibuf& msg)
{
  mystring tmp;
  msg.getline(tmp);
  docmd("MAIL FROM:<" + tmp + ">", 200);
  while(msg.getline(tmp) && !!tmp)
    docmd("RCPT TO:<" + tmp + ">", 200);
}

void smtp::send_data(fdibuf& msg)
{
  docmd("DATA", 300);
  mystring tmp;
  while(msg.getline(tmp)) {
    if((tmp[0] == '.' && !(out << ".")) ||
       !(out << tmp << "\r\n"))
      protocol_fail(ERR_MSG_WRITE, "Error sending message to remote");
  }
  docmd(".", 200, tmp);
  out << "QUIT\r\n";
  out.flush();
  protocol_succ(tmp.c_str());
}

void smtp::send(fdibuf& msg)
{
  send_envelope(msg);
  send_data(msg);
}

void protocol_prep(fdibuf&)
{
}

static int did_starttls = 0;

void protocol_starttls(fdibuf& netin, fdobuf& netout)
{
  smtp conn(netin, netout);
  conn.docmd("", 200);
  conn.dohelo(true);
  conn.docmd("STARTTLS", 200);
  did_starttls = 1;
}

void protocol_send(fdibuf& in, fdibuf& netin, fdobuf& netout)
{
  smtp conn(netin, netout);
  if (!did_starttls)
    conn.docmd("", 200);

  if (auth_method == AUTH_EXTERNAL) {
    conn.dohelo(true);
    if (conn.hascap("AUTH", "EXTERNAL"))
      conn.docmd("AUTH EXTERNAL =", 200, ERR_AUTH_FAILED);
    else
      protocol_fail(ERR_MSG_TEMPFAIL, "Server does not advertise certificate authentication");
  }
  else if (user != 0 && pass != 0) {
    conn.dohelo(true);
    if (auth_method == AUTH_LOGIN)
      conn.auth_login();
    else if (auth_method == AUTH_PLAIN)
      conn.auth_plain();
    else {
      // Detect method
      if (conn.hascap("AUTH", "PLAIN"))
	conn.auth_plain();
      else if (conn.hascap("AUTH", "LOGIN"))
	conn.auth_login();
      else
	protocol_fail(ERR_MSG_TEMPFAIL, "Server does not advertise any supported authentication methods");
    }
  }
  else
    conn.dohelo(false);

  conn.send(in);
}
