// ---------------------------------------------------------------------------
// - Mail.cpp                                                                -
// - afnix:net module - mail class implementation                            - 
// ---------------------------------------------------------------------------
// - This program is free software;  you can redistribute it  and/or  modify -
// - it provided that this copyright notice is kept intact.                  -
// -                                                                         -
// - 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.  In no event shall -
// - the copyright holder be liable for any  direct, indirect, incidental or -
// - special damages arising in any way out of the use of this software.     -
// ---------------------------------------------------------------------------
// - copyright (c) 1999-2008 amaury darsch                                   -
// ---------------------------------------------------------------------------

#include "Mail.hpp"
#include "Regex.hpp"
#include "Vector.hpp"
#include "System.hpp"
#include "Integer.hpp"
#include "QuarkZone.hpp"

namespace afnix {

  // -------------------------------------------------------------------------
  // - private section                                                       -
  // -------------------------------------------------------------------------

  // default mta constant
  const String  DEF_MTA_ADDR = System::hostname ();
  const t_word  DEF_MTA_PORT = 25;

  // mta commands
  const String& MTA_CMD_HELO = "HELO ";
  const String& MTA_CMD_MAIL = "MAIL FROM: ";
  const String& MTA_CMD_RCPT = "RCPT TO: ";
  const String& MTA_CMD_QUIT = "QUIT";
  const String& MTA_CMD_SMSG = "DATA";
  const String& MTA_CMD_EMSG = ".";

  // header constants
  const String& MTA_HDR_FROM = "From: ";
  const String& MTA_HDR_TORL = "To: ";
  const String& MTA_HDR_CCRL = "Cc: ";
  const String& MTA_HDR_SUBJ = "Subject: ";

  // this procedure returns the host canonical name
  static String get_canonical_name (void) {
    String  host = System::hostname ();
    Address addr = host;
    return addr.getcanon ();
  }

  // this procedure send a command to the mta and check the status code
  // if the status code is not valid, an exception is raised
  static void mta_send (TcpClient& mta, const String& cmd) {
    // send the command
    mta.writeln (cmd);
    // read the reply
    String reply = mta.readln ();
    // extract the status code
    Regex rgx ("($d$d$d)($N*)");
    if (rgx == reply) {
      long code = rgx.getint (0);
      if ((code < 200) || (code > 399))
	throw Exception ("mta-error", rgx.getstr (1));
    } else {
      throw Exception ("mta-error", "invalid reply message", reply);
    }
  }
  
  // this procedure adds recipients to the recipients list
  static void mta_rcpt (TcpClient& mta, const Strvec& rl) {
    Regex rgx ("$N*($<<$a-+.@>+$>)");
    long len = rl.length ();
    for (long i = 0; i < len; i++) {
      String addr = rl.get (i);
      String rcpt = "<";
      if (rgx == addr) 
	rcpt = rgx.getstr (0);
      else
	rcpt = rcpt + addr + '>';
      mta_send (mta, MTA_CMD_RCPT + rcpt);
    }
  }

  // -------------------------------------------------------------------------
  // - class section                                                         -
  // -------------------------------------------------------------------------

  // create a default mail with no subject

  Mail::Mail (void) {
    // initialize the mta address
    d_addr = DEF_MTA_ADDR;
    d_port = DEF_MTA_PORT;
    // get host canonical name
    d_host = get_canonical_name ();
    // get the mail from address
    d_from = System::username () + '@' + d_host;
    // set no subject
    d_subj = "no subject";
  }

  // return the class name

  String Mail::repr (void) const {
    return "Mail";
  }

  // set the mta address

  void Mail::setaddr (const String& addr) {
    wrlock ();
    d_addr = addr.strip ();
    unlock ();
  }

  // get the mta address

  String Mail::getaddr (void) const {
    rdlock ();
    String addr = d_addr;
    unlock ();
    return addr;
  }

  // set the mta port

  void Mail::setport (const t_word port) {
    wrlock ();
    d_port = port;
    unlock ();
  }

  // get the mta port

  t_word Mail::getport (void) const {
    rdlock ();
    t_word port = d_port;
    unlock ();
    return port;
  }

  // add a recipient to the "to" list

  void Mail::addto (const String& rcpt) {
    wrlock ();
    Strvec data = Strvec::split (rcpt, ",");
    long len = data.length ();
    for (long i = 0; i < len; i++) {
      String addr = data.get (i);
      d_torl.add (addr.strip ());
    }
    unlock ();
  }

  // add a recipient to the "cc" list

  void Mail::addcc (const String& rcpt) {
    wrlock ();
    Strvec data = Strvec::split (rcpt, ",");
    long len = data.length ();
    for (long i = 0; i < len; i++) {
      String addr = data.get (i);
      d_ccrl.add (addr.strip ());
    }
    unlock ();
  }

  // add a recipient to the "bcc" list

  void Mail::addbcc (const String& rcpt) {
    wrlock ();
    Strvec data = Strvec::split (rcpt, ",");
    long len = data.length ();
    for (long i = 0; i < len; i++) {
      String addr = data.get (i);
      d_bcrl.add (addr.strip ());
    }
    unlock ();
  }

  // set the mail subject

  void Mail::setsubj (const String& subj) {
    wrlock ();
    d_subj = subj;
    unlock ();
  }

  // add a line to the message buffer

  void Mail::addmsg (const String& data) {
    wrlock ();
    d_mesg.add (data);
    unlock ();
  }

  // send an email by connecting to the mta
  // this operation is done in two step - first prepare the mail header
  // second, connect to the mta and send the message. 

  void Mail::send (void) const {
    rdlock ();
    Buffer header;
    try {
      // get the header
      gethead (header);
      // establish a connection with the mta
      TcpClient mta (d_addr, d_port);
      // annouce ourself
      mta_send (mta, MTA_CMD_HELO + d_host);
      mta_send (mta, MTA_CMD_MAIL + d_from);
      // loop in the recipient list
      mta_rcpt (mta, d_torl);
      mta_rcpt (mta, d_ccrl);
      mta_rcpt (mta, d_bcrl);
      // start to send the message
      mta_send (mta, MTA_CMD_SMSG);
      // write the header
      header.write (mta);
      // write the message
      d_mesg.write (mta);
      // end the message
      mta_send (mta, MTA_CMD_EMSG);
      // quit
      mta_send (mta, MTA_CMD_QUIT);
      // cleanup and return
      mta.close ();
      unlock ();
    } catch (...) {
      unlock ();
      throw;
    }
  }

  // get the mail header (private)

  void Mail::gethead (Buffer& head) const {
    // from:
    head.add (MTA_HDR_FROM + System::username ());
    head.add (eolc);
    // add the subject
    if (d_subj.length () != 0) {
      head.add (MTA_HDR_SUBJ + d_subj);
      head.add (eolc);
    }
    // prepare the to list
    String torl;
    long len = d_torl.length ();
    for (long i = 0; i < len; i++) {
      String addr = d_torl.get (i);
      torl = (i == 0) ? addr : torl + ',' + addr;
    }
    if (torl.length () != 0) {
      head.add (MTA_HDR_TORL + torl);
      head.add (eolc);
    }
    // preapre the cc list
    String ccrl;
    len = d_ccrl.length ();
    for (long i = 0; i < len; i++) {
      String addr = d_ccrl.get (i);
      ccrl = (i == 0) ? addr : ccrl + ',' + addr;
    }
    if (ccrl.length () != 0) {
      head.add (MTA_HDR_CCRL + ccrl);
      head.add (eolc);
    }
    // finish the header with a newline
    head.add (eolc);
  }

  // -------------------------------------------------------------------------
  // - object section                                                        -
  // -------------------------------------------------------------------------

  // the quark zone
  static const long QUARK_ZONE_LENGTH = 11;
  static QuarkZone  zone (QUARK_ZONE_LENGTH);

  // the object supported quarks
  static const long QUARK_TO      = zone.intern ("to");
  static const long QUARK_CC      = zone.intern ("cc");
  static const long QUARK_BCC     = zone.intern ("bcc");
  static const long QUARK_ADD     = zone.intern ("add");
  static const long QUARK_SEND    = zone.intern ("send");
  static const long QUARK_ADDLN   = zone.intern ("addln");
  static const long QUARK_SUBJECT = zone.intern ("subject");
  static const long QUARK_GETPORT = zone.intern ("get-mta-port");
  static const long QUARK_GETADDR = zone.intern ("get-mta-address");
  static const long QUARK_SETPORT = zone.intern ("set-mta-port");
  static const long QUARK_SETADDR = zone.intern ("set-mta-address");

  // create a new object in a generic way
 
  Object* Mail::mknew (Vector* argv) {
    long argc = (argv == nilp) ? 0 : argv->length ();
    // check for 0 argument
    if (argc == 0) return new Mail;
    // too many arguments
    throw Exception ("argument-error",
                     "too many argument with mail constructor");
  }
 
  // return true if the given quark is defined

  bool Mail::isquark (const long quark, const bool hflg) const {
    rdlock ();
    if (zone.exists (quark) == true) {
      unlock ();
      return true;
    }
    bool result = hflg ? Object::isquark (quark, hflg) : false;
    unlock ();
    return result;
  }

  // apply thsi object with a set of arguments and a quark
 
  Object* Mail::apply (Runnable* robj, Nameset* nset, const long quark,
		       Vector* argv) {
    // get the number of arguments
    long argc = (argv == nilp) ? 0 : argv->length ();
 
    // dispatch variable length arguments
    if ((quark == QUARK_ADD) || (quark == QUARK_ADDLN)) {
      String result;
      for (long i = 0; i < argc; i++) {
	Object*   obj = argv->get (i);
	Literal* lobj = dynamic_cast <Literal*> (obj);
	if (lobj == nilp) 
	  throw Exception ("type-error", "invalid object to add", 
			   Object::repr (obj));
	result = result + lobj->tostring ();
      }
      if (quark == QUARK_ADDLN)  result = result + eolc;
      addmsg (result);
      return nilp;
    }

    // check for 0 argument
    if (argc == 0) {
      if (quark == QUARK_GETADDR) return new String  (getaddr ());
      if (quark == QUARK_GETPORT) return new Integer (getport ());
      if (quark == QUARK_SEND) {
	send ();
	return nilp;
      }
    }
    // check for 1 argument
    if (argc == 1) {
      if (quark == QUARK_TO) {
	String rcpt = argv->getstring (0);
	addto (rcpt);
	return nilp;
      }
      if (quark == QUARK_CC) {
	String rcpt = argv->getstring (0);
	addcc (rcpt);
	return nilp;
      }
      if (quark == QUARK_BCC) {
	String rcpt = argv->getstring (0);
	addbcc (rcpt);
	return nilp;
      }
      if (quark == QUARK_SUBJECT) {
	String subj = argv->getstring (0);
	setsubj (subj);
	return nilp;
      }
      if (quark == QUARK_SETADDR) {
	String addr = argv->getstring (0);
	setaddr (addr);
	return nilp;
      }
      if (quark == QUARK_SETPORT) {
	t_word port = argv->getint (0);
	setport (port);
	return nilp;
      }
    }
    // call the object method
    return Object::apply (robj, nset, quark, argv);
  }
}
