/*
 * ratSMTP.c --
 *
 *	This file contains basic support for sending messages via SMTP.
 *
 * TkRat software and its included text is Copyright 1996-2002 by
 * Martin Forssén
 *
 * The full text of the legal notice is contained in the file called
 * COPYRIGHT, included with this distribution.
 */

#include "rat.h"

/*
 * Each channel has one of these structures. The channel handler is actually
 * just a pointer to this structure.
 */
typedef struct SMTPChannelPriv {
    Tcl_Channel channel;
    unsigned int mime : 1;	/* True if the peer supports 8 bit mime */
    unsigned int dsn : 1;	/* True if the peer supports DSN */
} SMTPChannelPriv;

/*
 * Linked list of cached channels
 */
typedef struct ChannelCache {
    SMTPChannelPriv *chPtr;
    char *host;
    int port;
    struct ChannelCache *next;
} ChannelCache;
static ChannelCache *channelCache = NULL;
 
/*
 * Local functions
 */
static char *RatTimedGets(Tcl_Interp *interp, Tcl_Channel channel,int timeout);
static int RatSendCommand(Tcl_Interp *interp, Tcl_Channel channel, char *cmd);
static int RatSendRcpt(Tcl_Interp *interp, Tcl_Channel channel,
	ADDRESS *adrPtr, DSNhandle handle, int verbose);


/*
 *----------------------------------------------------------------------
 *
 * RatTimedGets --
 *
 *      A gets with timeout. the channel must be in nonblocking mode.
 *
 * Results:
 *	Returns a pointer to a static area containing the read string.
 *
 * Side effects:
 *	The previous result is overwritten
 *
 *
 *----------------------------------------------------------------------
 */

static char*
RatTimedGets(Tcl_Interp *interp, Tcl_Channel channel, int timeout)
{
    static Tcl_DString ds;
    static int dsInit = 0;

    if (!dsInit) {
	dsInit = 1;
	Tcl_DStringInit(&ds);
    } else {
	Tcl_DStringSetLength(&ds, 0);
    }

    Tcl_SetChannelOption(interp, channel, "-blocking", "0");
    while (-1 == Tcl_Gets(channel, &ds)) {
	if (Tcl_InputBlocked(channel) && timeout) {
	    sleep(1),
	    timeout--;
	} else {
	    Tcl_SetChannelOption(interp, channel, "-blocking", "1");
	    return NULL;
	}
    }
    Tcl_SetChannelOption(interp, channel, "-blocking", "1");
    return Tcl_DStringValue(&ds);
}

/*
 *----------------------------------------------------------------------
 *
 * RatSendCommand --
 *
 *      Send a command to the SMTP peer, wait for and parse the result.
 *
 * Results:
 *	A standard Tcl result and an eventual error messages in 
 *	the result area.
 *
 * Side effects:
 *	None.
 *
 *
 *----------------------------------------------------------------------
 */

int
RatSendCommand(Tcl_Interp *interp, Tcl_Channel channel, char *cmd)
{
    int result, timeout;
    Tcl_Obj *oPtr;
    char *reply;

    Tcl_Write(channel, cmd, -1);
    if ('\n' != cmd[strlen(cmd)-1]) {
	Tcl_Write(channel, "\r\n", -1);
    }
    oPtr = Tcl_GetVar2Ex(interp, "option", "smtp_timeout", TCL_GLOBAL_ONLY);
    Tcl_GetIntFromObj(interp, oPtr, &timeout);
    do {
	reply = RatTimedGets(interp, channel, timeout);
	if (reply) {
	    if ('2' == reply[0] || '3' == reply[0]) {
		result = TCL_OK;
	    } else {
		Tcl_SetResult(interp, reply, TCL_VOLATILE);
		result = TCL_ERROR;
	    }
	} else {
	    Tcl_SetResult(interp, "Timeout from SMTP server", TCL_STATIC);
	    result = TCL_ERROR;
	}
    } while (TCL_OK == result && '-' == reply[4]);
    return result;
}

/*
 *----------------------------------------------------------------------
 *
 * RatSendRcpt --
 *
 *      Send the RCPT TO statements to the SMTP peer.
 *
 * Results:
 *	Returns the number of failed addresses. The result string of interp
 *	will contain more information in case of failure.
 *
 * Side effects:
 *	None.
 *
 *
 *----------------------------------------------------------------------
 */

static int
RatSendRcpt(Tcl_Interp *interp, Tcl_Channel channel, ADDRESS *adrPtr,
	DSNhandle handle, int verbose)
{
    char buf[2048], adr[1024];
    unsigned char *cPtr;
    int failures = 0, i;

    for (; adrPtr; adrPtr = adrPtr->next) {
	if (RatAddressSize(adrPtr, 0) > sizeof(adr)) {
	    RatLogF(interp, RAT_WARN, "ridiculously_long", RATLOG_TIME);
	    failures++;
	}
	adr[0] = '\0';
	rfc822_address(adr, adrPtr);
	snprintf(buf, sizeof(buf), "RCPT TO:<%s>", adr);
	if (handle) {
	    RatDSNAddRecipient(interp, handle,  adr);
	    snprintf(buf+strlen(buf), sizeof(buf)-strlen(buf),
		    " NOTIFY=SUCCESS,FAILURE,DELAY");
	    snprintf(buf+strlen(buf), sizeof(buf)-strlen(buf),
		    " ORCPT=rfc822;");
	    for (i = strlen(buf),cPtr = (unsigned char*)adr; *cPtr; cPtr++) {
		if (*cPtr < 33 || *cPtr > 126 || *cPtr == '+' || *cPtr == '='){
		    buf[i++] = '+';
		    buf[i++] = alphabetHEX[*cPtr>>4];
		    buf[i++] = alphabetHEX[*cPtr&0xf];
		} else {
		    buf[i++] = *cPtr;
		}
	    }
	    buf[i] = '\0';
	}
	if (3 == verbose) {
	    RatLogF(interp, RAT_PARSE, "send_rcpt", RATLOG_EXPLICIT, adr);
	}
	if (TCL_OK != RatSendCommand(interp, channel, buf)) {
	    failures++;
	}
    }
    return failures;
}

/*
 *----------------------------------------------------------------------
 *
 * RatSMTPOpen --
 *
 *      Open an SMTP channel.
 *
 * Results:
 *	Returns a channel handler.
 *
 * Side effects:
 *	A new channel is created.
 *
 *
 *----------------------------------------------------------------------
 */

SMTPChannel
RatSMTPOpen (Tcl_Interp *interp, char *host, int verbose, const char *role)
{
    SMTPChannelPriv *chPtr;
    char *reply, buf[1024], *cPtr, *ch;
    int port, timeout;
    ChannelCache *cachePtr;
    Tcl_Obj *oPtr;

    strlcpy(buf, host, sizeof(buf));
    if ((cPtr = strchr(buf, ':'))) {
	*cPtr++ = '\0';
	port = atoi(cPtr);
    } else {
	port = 25;	/* The default SMTP port */
    }

    for (cachePtr = channelCache; cachePtr; cachePtr = cachePtr->next) {
	if (!strcmp(cachePtr->host, buf) && cachePtr->port == port) {
	    if (TCL_OK == RatSendCommand(interp, cachePtr->chPtr->channel,
					 "RSET")) {
		return cachePtr->chPtr;
	    }
	    break;
	}
    }

    if (verbose > 1) {
	RatLogF(interp, RAT_PARSE, "opening_connection", RATLOG_EXPLICIT);
    }
    chPtr = (SMTPChannelPriv*)ckalloc(sizeof(SMTPChannelPriv));
    chPtr->mime = chPtr->dsn = 0;
    /*
     * Your compiler may complain that there are too many arguments to
     * Tcl_OpenTcpClient() below. This is a symtom of you having a prerelease
     * of tcl7.5 installed. In this case you must upgrade to the real
     * releases of tcl7.5/tk4.1 and reconfigure tkrat (this MUST be done).
     * After rerunning configure you may build tkrat.
     */
    if (NULL == (chPtr->channel =Tcl_OpenTcpClient(interp,port,buf,NULL,0,0))){
	ckfree(chPtr);
	return NULL;
    }
    Tcl_SetChannelOption(interp, chPtr->channel, "-buffering", "line");
    Tcl_SetChannelOption(interp, chPtr->channel, "-translation", "binary");

    /*
     * Get initial greeting
     */
    if (verbose > 1) {
	RatLogF(interp, RAT_PARSE, "wait_greeting", RATLOG_EXPLICIT);
    }
    oPtr = Tcl_GetVar2Ex(interp, "option", "smtp_timeout", TCL_GLOBAL_ONLY);
    Tcl_GetIntFromObj(interp, oPtr, &timeout);
    do {
	reply = RatTimedGets(interp, chPtr->channel, timeout);
	if (!reply || '2' != reply[0]) {
	    Tcl_Close(interp, chPtr->channel);
	    ckfree(chPtr);
	    return NULL;
	}
    } while (strncmp("220 ", reply, 4));

    /*
     * Send EHLO (HELO) and get capabilities
     */
    if (verbose > 1) {
	RatLogF(interp, RAT_PARSE, "get_capabilities", RATLOG_EXPLICIT);
    }
    ch = RatGetCurrent(interp, RAT_HOST, role);
    snprintf(buf, sizeof(buf), "EHLO %s\r\n", ch);
    Tcl_Write(chPtr->channel, buf, -1);
    reply = RatTimedGets(interp, chPtr->channel, timeout);
    if (!reply || '2' != reply[0]) {
	snprintf(buf, sizeof(buf), "HELO %s\r\n", ch);
	Tcl_Write(chPtr->channel, buf, -1);
	reply = RatTimedGets(interp, chPtr->channel, timeout);
    }
    while (reply) {
	if (!reply) {
	    Tcl_Close(interp, chPtr->channel);
	    ckfree(chPtr);
	    return NULL;
	}
	if (!strncmp("8BITMIME", &reply[4], 8)) {
	    chPtr->mime = 1;
	} else if (!strncmp("DSN", &reply[4], 3)) {
	    chPtr->dsn = 1;
	}
	if (!strncmp("250 ", reply, 4)) {
	    break;
	}
	reply = RatTimedGets(interp, chPtr->channel, timeout);
    }

    if (verbose > 1) {
	RatLog(interp, RAT_PARSE, "", RATLOG_EXPLICIT);
    }

    cachePtr = (ChannelCache*)ckalloc(sizeof(ChannelCache)+strlen(host)+1);
    cachePtr->chPtr = chPtr;
    cachePtr->host = (char*)cachePtr + sizeof(*cachePtr);
    strlcpy(cachePtr->host, host, strlen(host));
    cachePtr->port = port;
    cachePtr->next = channelCache;
    channelCache = cachePtr;
    
    return (SMTPChannel)chPtr;
}

/*
 *----------------------------------------------------------------------
 *
 * RatSMTPClose --
 *
 *      Close an SMTP channel.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	THe chanel is closed.
 *
 *
 *----------------------------------------------------------------------
 */

void
RatSMTPClose (Tcl_Interp *interp, SMTPChannel channel, int verbose)
{
    SMTPChannelPriv *chPtr = (SMTPChannelPriv*)channel;
    ChannelCache **c1Ptr, *c2Ptr;

    /*
     * Close connection
     */
    if (verbose > 1) {
	RatLogF(interp, RAT_PARSE, "closing", RATLOG_EXPLICIT);
    }
    Tcl_Write(chPtr->channel, "QUIT\r\n", -1);
    Tcl_Close(interp, chPtr->channel);
    if (verbose > 1) {
	RatLog(interp, RAT_PARSE, "", RATLOG_EXPLICIT);
    }
    ckfree(chPtr);

    /*
     * Clear cache
     */
    for (c1Ptr = &channelCache; *c1Ptr && (*c1Ptr)->chPtr != chPtr;
	 c1Ptr = &(*c1Ptr)->next);
    if (*c1Ptr) {
	c2Ptr = (*c1Ptr)->next;
	ckfree(*c1Ptr);
	*c1Ptr = c2Ptr;
    }
}

/*
 *----------------------------------------------------------------------
 *
 * RatSMTPClose --
 *
 *      Close an SMTP channel.
 *
 * Results:
 *	None.
 *
 * Side effects:
 *	THe chanel is closed.
 *
 *
 *----------------------------------------------------------------------
 */

void
RatSMTPCloseAll (Tcl_Interp *interp, int verbose)
{
    while (channelCache) {
	RatSMTPClose(interp, channelCache->chPtr, verbose);
    }
}

/*
 *----------------------------------------------------------------------
 *
 * RatSMTPSend --
 *
 *      Send a message with SMTP over the specified channel.
 *
 * Results:
 *	A standard Tcl result and an eventual error messages in 
 *	the result area.
 *
 * Side effects:
 *	The DSN structures may be updated.
 *
 *
 *----------------------------------------------------------------------
 */

int
RatSMTPSend (Tcl_Interp *interp, SMTPChannel channel, ENVELOPE *envPtr,
	BODY *bodyPtr, int doDSN, int verbose)
{
    SMTPChannelPriv *chPtr = (SMTPChannelPriv*)channel;
    char buf[1024], *header;
    int failures = 0;
    DSNhandle handle = NULL;

    /*
     * Check input and reset stream to known status.
     */
    if (!(envPtr->to || envPtr->cc || envPtr->bcc)) {
	Tcl_SetResult(interp, "No recipients specified", TCL_STATIC);
	goto abort;
    }
    if (TCL_OK != RatSendCommand(interp, chPtr->channel, "RSET")) {
	goto abort;
    }

    /*
     * Check if we should request DSN
     */
    if (doDSN && !chPtr->dsn) {
	RatLogF(interp, RAT_WARN, "no_dsn", RATLOG_TIME);
	doDSN = 0;
    }

    /*
     * Send envelope information.
     */
    if (verbose > 1) {
	if (verbose == 2) {
	    RatLogF(interp, RAT_PARSE, "send_envelope", RATLOG_EXPLICIT);
	} else {
	    RatLogF(interp, RAT_PARSE, "send_from", RATLOG_EXPLICIT);
	}
    }
    if (RatAddressSize(envPtr->from, 0) > sizeof(buf)-128) {
	RatLogF(interp, RAT_WARN, "ridiculously_long", RATLOG_TIME);
	goto abort;
    }
    snprintf(buf, sizeof(buf), "MAIL FROM:<");
    rfc822_address(buf, envPtr->from);
    strlcat(buf, ">", sizeof(buf));
    if (chPtr->mime) {
	strlcat(buf, " BODY=8BITMIME", sizeof(buf));
    }
    if (doDSN) {
	RatGenId(NULL, interp, 0, NULL);
	handle = RatDSNStartMessage(interp, Tcl_GetStringResult(interp),
				    envPtr->subject);
	strlcat(buf, " ENVID=", sizeof(buf));
	strlcat(buf, Tcl_GetStringResult(interp), sizeof(buf));
    }
    if (TCL_OK != RatSendCommand(interp, chPtr->channel, buf)) {
	goto abort;
    }
    failures += RatSendRcpt(interp, chPtr->channel,envPtr->to,handle,verbose);
    failures += RatSendRcpt(interp, chPtr->channel,envPtr->cc,handle,verbose);
    failures += RatSendRcpt(interp, chPtr->channel,envPtr->bcc,handle,verbose);
    if (failures) {
	goto abort;
    }

    /*
     * Send message data
     */
    if (verbose > 1) {
	RatLogF(interp, RAT_PARSE, "send_data", RATLOG_EXPLICIT);
    }
    if (TCL_OK != RatSendCommand(interp, chPtr->channel, "DATA")) {
	goto abort;
    }
    header = (char*)ckalloc(RatHeaderSize(envPtr, bodyPtr));
    rfc822_output(header, envPtr, bodyPtr, RatTclPutsSMTP, chPtr->channel,
	    chPtr->mime);
    ckfree(header);
    if (verbose > 1) {
	RatLogF(interp, RAT_PARSE, "wait_ack", RATLOG_EXPLICIT);
    }
    if (TCL_OK != RatSendCommand(interp, chPtr->channel, ".")) {
	goto abort;
    }
    if (handle) {
	RatDSNFinish(interp, handle);
    }

    return TCL_OK;

 abort:
    RatDSNAbort(interp, handle);
    return TCL_ERROR;
}

/*
 *----------------------------------------------------------------------
 *
 * RatSMTPSupportDSN --
 *
 *      Check if a host supports DSN.
 *
 * Results:
 *	TCL_OK and the result area will contain "1" if the host supports
 *	DSN and "0" otherwise.
 *
 * Side effects:
 *	None.
 *
 *
 *----------------------------------------------------------------------
 */

int
RatSMTPSupportDSN(ClientData dummy, Tcl_Interp *interp, int objc,
		  Tcl_Obj *const objv[])
{
    SMTPChannelPriv *chPrivPtr;
    SMTPChannel channel;
    int verbose, result;

    if (objc != 2) {
	Tcl_AppendResult(interp, "wrong # args: should be \"",
			 Tcl_GetString(objv[0]), " hostname\"",
			 (char *) NULL);
	return TCL_ERROR;
    }

    Tcl_GetIntFromObj(interp,
		      Tcl_GetVar2Ex(interp, "option", "smtp_verbose",
				    TCL_GLOBAL_ONLY),
		      &verbose);
    channel = RatSMTPOpen(interp, Tcl_GetString(objv[1]), verbose, "");
    if (channel) {
	chPrivPtr = (SMTPChannelPriv*)channel;
	result = chPrivPtr->dsn;
	RatSMTPClose(interp, channel, verbose);
    } else {
	result = 0;
    }
    if (verbose) {
	RatLog(interp, RAT_PARSE, "", RATLOG_EXPLICIT);
    }
    Tcl_SetObjResult(interp, Tcl_NewIntObj(result));
    return TCL_OK;
}

/*
 *----------------------------------------------------------------------
 *
 * RatTclPutsSMTP --
 *
 *	Like RatTclPuts but also escapes sigle dots with double dots.
 *
 * Results:
 *      Always returns 1L.
 *
 * Side effects:
 *      None.
 *
 *
 *----------------------------------------------------------------------
 */

long
RatTclPutsSMTP(void *stream_x, char *string)
{
    Tcl_Channel channel = (Tcl_Channel)stream_x;
    char *cPtr, *srcPtr;

    if ('.' == string[0]) {
	Tcl_Write(channel, ".", 1);
    }

    for (srcPtr = string; *srcPtr;) {
	if (!srcPtr[0] || !srcPtr[1] || !srcPtr[2]) {
	    break;
	}
	for (cPtr = srcPtr; cPtr[2]; cPtr++) {
	    if ('\r' == cPtr[0] && '\n' == cPtr[1] && '.' == cPtr[2]) {
		break;
	    }
	}
	if (cPtr[2]) {
	    if (-1 == Tcl_Write(channel, srcPtr, cPtr-srcPtr+3)
		    || -1 == Tcl_Write(channel, ".", 1)) {
		return 0;
	    }
	    srcPtr = cPtr+3;
	} else {
	    break;
	}
    }
    if (-1 == Tcl_Write(channel, srcPtr, -1)) {
	return 0;
    } else {
	return 1;
    }
}

/*
 *----------------------------------------------------------------------
 *
 * RatTclPutsSendmail --
 *
 *	Like RatTclPutsSMTP but also escapes changes line endings to
 *	put \n
 *
 * Results:
 *      Always returns 1L.
 *
 * Side effects:
 *      None.
 *
 *
 *----------------------------------------------------------------------
 */

long
RatTclPutsSendmail(void *stream_x, char *string)
{
    Tcl_Channel channel = (Tcl_Channel)stream_x;
    char *cPtr, *srcPtr;
    int add;

    for (srcPtr = string; *srcPtr;) {
	if (!srcPtr[0] || !srcPtr[1]) {
	    break;
	}
	add = 1;
	for (cPtr = srcPtr; cPtr[1]; cPtr++) {
	    if ('\r' == cPtr[0] && '\n' == cPtr[1]) {
		cPtr--;
		add = 2;
		break;
	    }
	}
	if (-1 == Tcl_Write(channel, srcPtr, (cPtr+1)-srcPtr)) {
	    return 0;
	}
	srcPtr = cPtr+add;
    }
    if (*srcPtr && -1 == Tcl_Write(channel, srcPtr, -1)) {
	return 0;
    } else {
	return 1;
    }
}

/*
 *----------------------------------------------------------------------
 *
 * RatHeaderSize --
 *
 *	Calculate size of header
 *
 * Results:
 *      Maximum size of header
 *
 * Side effects:
 *      None.
 *
 *
 *----------------------------------------------------------------------
 */

static int RatHeaderLineSize(char *name, ENVELOPE *env, char *text);
static int RatHeaderAddressSize(char *name, ENVELOPE *env, ADDRESS *adr);

static int
RatHeaderLineSize(char *name, ENVELOPE *env, char *text)
{
    if (text) {
	return (env->remail ? 7 : 0) + strlen(name) + 2 + strlen(text) + 2;
    } else {
	return 0;
    }
}

static int
RatHeaderAddressSize(char *name, ENVELOPE *env, ADDRESS *adr)
{
    if (adr) {
	return (env->remail?7:0) + strlen(name) + 2 + RatAddressSize(adr, 1)+2;
    } else {
	return 0;
    }
}

size_t
RatHeaderSize(ENVELOPE *env,BODY *body)
{
    size_t len = 0;

    if (env->remail) len += strlen(env->remail);
    len += RatHeaderLineSize("Newsgroups", env, env->newsgroups);
    len += RatHeaderLineSize("Date", env, env->date); 
    len += RatHeaderAddressSize("From", env, env->from);
    len += RatHeaderAddressSize("Sender", env, env->sender);
    len += RatHeaderAddressSize("Reply-To", env, env->reply_to);
    len += RatHeaderLineSize("Subject", env, env->subject);
    if (env->bcc && !(env->to || env->cc)) {
	len += strlen("To: undisclosed recipients: ;\015\012");
    }
    len += RatHeaderAddressSize("To", env, env->to);
    len += RatHeaderAddressSize("cc", env, env->cc);
    len += RatHeaderLineSize("In-Reply-To", env, env->in_reply_to);
    len += RatHeaderLineSize("Message-ID", env, env->message_id);
    len += RatHeaderLineSize("Followup-to", env, env->followup_to);
    len += RatHeaderLineSize("References", env, env->references);
    if (body && !env->remail) {   /* not if remail or no body structure */
	/*
	 * TODO: Fix this correctly
	 * Here we assume that the body headers will never become longer
	 * than 8192 bytes
	 */
        len += 8192;
    } 
    len += 2;
    return len;
}


syntax highlighted by Code2HTML, v. 0.9.1