/*
** SellaNMS
** $Id: smtp.cxx,v 1.9 2006/01/24 07:08:09 sella Exp $
** Copyright (c) 2001-2005 Digital Genesis Software, LLC. All Rights Reserved.
** Released under the GPL Version 2 License.
** http://www.digitalgenesis.com
*/

static const char rcsid[] = "$Id: smtp.cxx,v 1.9 2006/01/24 07:08:09 sella Exp $";

#include "smtp.h"
#include "util.h"
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <syslog.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <netdb.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <stl/string>

/* 0-99 - Envelope */
/* 100-199 - Body */
/* 200-299 - Attachments */
/* 300 - End SMTP Session */

//extern int errno;

SMTP::SMTP():debug(false), useSyslog(false), severity(LOG_DEBUG), data(NULL) {
	clear();
}

SMTP::~SMTP() {
	if (data) {
		delete data;
		data = NULL;
	}
}
void SMTP::clear() {
	if (data)
		delete data;
	data = new smtp_data_t;

	setServer(DGS_SMTP_SERVER_DEF);
	setPort(DGS_SMTP_PORT_DEF);
	setTimeout(DGS_SMTP_TIMEOUT_DEF);
	setPriority(DGS_SMTP_PRIORITY_DEF);
	setLength(DGS_SMTP_LENGTH_DEF);
	length_subject = -1;

	hasRecipient = false;
	hasContent = false;

	smtp_pair.first = 99;
	snprintf(buf, sizeof(buf), "DATA\r\n");
	smtp_pair.second.first = buf;
	smtp_pair.second.second = 354;
	data->push(smtp_pair);

	smtp_pair.first = 100;
	smtp_pair.second.first = "Date: %a, %d %b %Y %H:%M:%S %z\r\n";
	smtp_pair.second.second = -1;
	data->push(smtp_pair);

	smtp_pair.first = 101;
	snprintf(buf, sizeof(buf), "X-Mailer: %s\r\n", DGS_SMTP_XMAILER_DEF);
	smtp_pair.second.first = buf;
	smtp_pair.second.second = -1;
	data->push(smtp_pair);

	smtp_pair.first = 198;
	snprintf(buf, sizeof(buf), "\r\n");
	smtp_pair.second.first = buf;
	smtp_pair.second.second = -1;
	data->push(smtp_pair);

	smtp_pair.first = 300;
	snprintf(buf, sizeof(buf), "\r\n.\r\n");
	smtp_pair.second.first = buf;
	smtp_pair.second.second = 250;
	data->push(smtp_pair);

	smtp_pair.first = 301;
	snprintf(buf, sizeof(buf), "QUIT\r\n");
	smtp_pair.second.first = buf;
	smtp_pair.second.second = 221;
	data->push(smtp_pair);
}

bool SMTP::isValid() {
	if (hasRecipient && hasContent) {
		return true;
	}

	return false;
}

RESULT SMTP::send() {
	RESULT result = FAILURE;
	
	struct hostent hent, *hentp = NULL;
	struct sockaddr_in socketaddr;
	int s = -1, err;
	unsigned int flags, errlen;
	struct timeval tv;
	fd_set wfds;
	smtp_data_t *tmp_data = NULL;

	/* Init socketaddr */
	bzero((char*) &socketaddr, sizeof(socketaddr));
	socketaddr.sin_family = AF_INET;
	socketaddr.sin_port = this->port;

	if (!isValid()) {
		if (this->useSyslog) {
			syslog(severity, "Error: Configuration is invalid.");
		} else {
			printf("Error: Configuration is invalid.\n");
		}
		return FAILURE;
	}

	/* Resolve Server */
#ifdef _REENTRANT
	gethostbyname_r(this->server.c_str(), &hent, buf, sizeof(buf), &hentp, &err);
#else
	hentp = gethostbyname(this->server.c_str());
#endif
	if (!hentp) {
		if (this->useSyslog) {
			syslog(severity, "Error: Unable to resolve hostname '%s'.", this->server.c_str());
		} else {
			printf("Error: Unable to resolve hostname '%s'.\n", this->server.c_str());
		}
		return FAILURE;
	}
	memcpy(&socketaddr.sin_addr, hentp->h_addr, hentp->h_length);

   s = socket(PF_INET, SOCK_STREAM, IPPROTO_TCP);
	if (s < 0) {
		if (this->useSyslog) {
			syslog(severity, "Error: Failed to create new socket.");
		} else {
			printf("Error: Failed to create new socket.\n");
		}
		return FAILURE;
	}

	/* Set socket to non-blocking. */
	flags = fcntl(s, F_GETFL, 0);
	fcntl(s, F_SETFL, O_NONBLOCK);

	tv.tv_sec = this->timeout;
	tv.tv_usec = 0;

	err = connect(s, (struct sockaddr *) &socketaddr, sizeof(socketaddr));
	if (err < 0 && errno != EINPROGRESS) {
		if (this->useSyslog) {
			syslog(severity, "Error: Failed to connect to '%s' on port %d.\n", this->server.c_str(), (ntohs(this->port)));
		} else {
			printf("Error: Failed to connect to '%s' on port %d.\n", this->server.c_str(), (ntohs(this->port)));
		}
	}

	FD_ZERO(&wfds);
	FD_SET(s, &wfds);

	/* Wait for <timeout> seconds for connection to go thru. */
	select(s + 1, NULL, &wfds, NULL, &tv);

	if (FD_ISSET(s, &wfds)) {
		/* Reset socket to blocking. */
		fcntl(s, F_SETFL, flags);

		/* Make sure socket really is ok to write. */
		errlen = sizeof(err);
		getsockopt(s, SOL_SOCKET, SO_ERROR, &err, &errlen);
		if (!err) {
			/* Create copy of data, since _send() modifies it. */
			tmp_data = new smtp_data_t(*data);

			/* Actually deliver the email. */
			result = _send(s, tmp_data);

			/* Free copy of data. */
			delete tmp_data;
		}
	}

	/* Close our socket. */
	close(s);
//	shutdown(s, 2);

	return result;
}

RESULT SMTP::_send(int s, smtp_data_t *data) {
	RESULT result = SUCCESS;

	bool body_empty = true;
	int response, cur_length, body_count = 0;
	time_t t = time(NULL);
	smtp_data_t *next_data = NULL;

	response = getResponse(s);
	if (response == 220 || response == 250) {
		if (length > 0) {
			next_data = new smtp_data_t;
			cur_length = length_subject;
		}

		/* Process Data */
		while (!data->empty()) {
			/* Send data */
			smtp_pair = data->top();

			if (next_data) {
				if (smtp_pair.first == 199) {
					if ((cur_length + smtp_pair.second.first.size() < length) || body_empty) { /* Chop up the body. */
						cur_length += smtp_pair.second.first.size();
					} else {
						body_count++;
						next_data->push(smtp_pair);
						
						data->pop();
						continue;
					}
					body_empty = false;
				} else if (smtp_pair.first == 301) {
					if (body_empty == true) {
						/* Stop recursion */
						delete next_data;
						next_data = NULL;
					} else {
						next_data->push(smtp_pair);

						data->pop();
						continue;
					}
				} else {
					next_data->push(smtp_pair);
				}
			}

			/* Set the Date/Time */
			if (smtp_pair.first == 100) {
				strftime(buf, sizeof(buf), smtp_pair.second.first.c_str(), localtime(&t));
				smtp_pair.second.first = buf;
			}
			if (this->debug) {
				if (this->useSyslog) {
					syslog(severity, "%03lu --> %s", smtp_pair.first, smtp_pair.second.first.c_str());
				} else {
					printf("%03lu --> %s\n", smtp_pair.first, smtp_pair.second.first.c_str());
					fflush(0);
				}
			}
			if (write(s, smtp_pair.second.first.c_str(), smtp_pair.second.first.size()) < 0) {
				if (this->useSyslog) {
					syslog(severity, "Error: Failed to write '%s' to socket.", smtp_pair.second.first.c_str());
				} else {
					printf("Error: Failed to write '%s' to socket.\n", smtp_pair.second.first.c_str());
				}
				break;
			}
	
			if (smtp_pair.second.second != -1) {
				/* Get Response */
				response = getResponse(s);
				if (response == 0) {
					if (this->useSyslog) {
						syslog(severity, "Error: Failed to read from socket.");
					} else {
						printf("Error: Failed to read from socket.\n");
					}
					result = FAILURE;
					break;
				}
		
				/* Check repsonce */
				if (response != smtp_pair.second.second) {
					if (this->useSyslog) {
						syslog(severity, "Error: Expecting %d but recieved %d. Full response: %s", smtp_pair.second.second, response, buf);
					} else {
						printf("Error: Expecting %d but recieved %d. Full response: %s\n", smtp_pair.second.second, response, buf);
					}
					result = FAILURE;

					/* Attempt to cleanup and disconnect gracefully. */
					if (write(s, "RSET\r\n", 6) == 6) {
						response = getResponse(s);
						if (response == 250) {
							write(s, "QUIT\r\n", 6);
						}
					}
					break;
				}
			}
	
			data->pop();
		}

		/* A little recursion.. */
		if (next_data) {
			if (body_count > 0 && write(s, "NOOP\r\n", 6) == 6) {
				_send(s, next_data);
			}
			delete next_data;
			next_data = NULL;
		}
	}

	return result;
}

int SMTP::getResponse(int s) {
	int count, response;

	/* Get response */
	count = read(s, buf, sizeof(buf));
	if (count < 0) {
		return 0;
	}
	if (this->debug) {
		buf[count] = '\0';
		if (this->useSyslog) {
			syslog(severity, "    <-- %s", buf);
		} else {
			printf("    <-- %s", buf);
			fflush(0);
		}
	}

	response = strtoul(buf, NULL, 10);

	return response;
}

void SMTP::setDebug(bool flag) {
	this->debug = flag;
}

void SMTP::setSyslog(bool flag) {
	this->useSyslog = flag;
}

void SMTP::setSeverity(int severity) {
	this->severity = severity;
}

void SMTP::setServer(string server) {
	cleanString(server);

	/* Remove previous server. */
	if (!data->empty() && data->top().first == 0) {
		data->pop();
	}

	this->server = server;

	smtp_pair.first = 0;
	snprintf(buf, sizeof(buf), "HELO %s\r\n", server.c_str());
	smtp_pair.second.first = buf;
	smtp_pair.second.second = 250;
	data->push(smtp_pair);
}

void SMTP::setPort(unsigned int port) {
	this->port = htons(port);
}

void SMTP::setTimeout(unsigned int timeout) {
	this->timeout = timeout;
}

void SMTP::setPriority(unsigned int priority) {
	if (priority >= 1 && priority <= 5) {
		smtp_pair.first = 108;
		snprintf(buf, sizeof(buf), "X-Priority: %d\r\n", priority);
		smtp_pair.second.first = buf;
		smtp_pair.second.second = -1;
		data->push(smtp_pair);
	}
}

void SMTP::setLength(unsigned int length) {
	this->length = length;
}

void SMTP::setSubject(string subject) {
	if (length_subject >= 0) {
		if (this->useSyslog) {
			syslog(severity, "Debug: The subject is already set. Call SMTP::clear().");
		} else {
			printf("Debug: The subject is already set. Call SMTP::clear().\n");
			fflush(0);
		}
		return;
	}

	cleanString(subject);

	smtp_pair.first = 107;
	snprintf(buf, sizeof(buf), "Subject: %s\r\n", subject.c_str());
	smtp_pair.second.first = buf;
	length_subject = smtp_pair.second.first.length();
	smtp_pair.second.second = -1;
	data->push(smtp_pair);

	hasContent = true;
}

void SMTP::setFrom(string from) {
	cleanString(from);

	smtp_pair.first = 1;
	snprintf(buf, sizeof(buf), "MAIL FROM: %s\r\n", from.c_str());
	smtp_pair.second.first = buf;
	smtp_pair.second.second = 250;
	data->push(smtp_pair);

	smtp_pair.first = 103;
	snprintf(buf, sizeof(buf), "From: %s\r\n", from.c_str());
	smtp_pair.second.first = buf;
	smtp_pair.second.second = -1;
	data->push(smtp_pair);
}

void SMTP::setReplyTo(string reply_to) {
	cleanString(reply_to);

	smtp_pair.first = 106;
	snprintf(buf, sizeof(buf), "Reply-To: %s\r\n", reply_to.c_str());
	smtp_pair.second.first = buf;
	smtp_pair.second.second = -1;
	data->push(smtp_pair);
}

void SMTP::setTo(string to) {
	cleanString(to);

	smtp_pair.first = 2;
	snprintf(buf, sizeof(buf), "RCPT TO: %s\r\n", to.c_str());
	smtp_pair.second.first = buf;
	smtp_pair.second.second = 250;
	data->push(smtp_pair);

	smtp_pair.first = 104;
	snprintf(buf, sizeof(buf), "To: %s\r\n", to.c_str());
	smtp_pair.second.first = buf;
	smtp_pair.second.second = -1;
	data->push(smtp_pair);

	hasRecipient = true;
}

void SMTP::setCc(string cc) {
	cleanString(cc);

	smtp_pair.first = 3;
	snprintf(buf, sizeof(buf), "RCPT TO: %s\r\n", cc.c_str());
	smtp_pair.second.first = buf;
	smtp_pair.second.second = 250;
	data->push(smtp_pair);
	
	smtp_pair.first = 105;
	snprintf(buf, sizeof(buf), "Cc: %s\r\n", cc.c_str());
	smtp_pair.second.first = buf;
	smtp_pair.second.second = -1;
	data->push(smtp_pair);

	hasRecipient = true;
}

void SMTP::setBcc(string bcc) {
	cleanString(bcc);

	smtp_pair.first = 4;
	snprintf(buf, sizeof(buf), "RCPT TO: %s\r\n", bcc.c_str());
	smtp_pair.second.first = buf;
	smtp_pair.second.second = 250;
	data->push(smtp_pair);

	hasRecipient = true;
}

void SMTP::setBody(string body) {
	correctBareLF(body);

	smtp_pair.first = 199;
	snprintf(buf, sizeof(buf), "%s\r\n", body.c_str());
	smtp_pair.second.first = buf;
	smtp_pair.second.second = -1;
	data->push(smtp_pair);

	hasContent = true;
}

bool SMTP::checkAddress() {
	/* Not done yet.. */
	return true;
}

void SMTP::correctBareLF(string& str) {
	char prev = 0;
	string::iterator it;
	
	/* Insert a \r before all bare \n. */
	for (it = str.begin(); it != str.end(); ++it) {
		if (*it == '\n' && prev != '\r') {
			it = str.insert(it, '\r');
			++it;
		}
		prev = *it;
	}
}

void SMTP::cleanString(string& str) {
	string::iterator it;
	
	/* Cut all characters following first \r or \n. */
	for (it = str.begin(); it != str.end(); ++it) {
		if (*it == '\n' || *it == '\r') {
			it = str.erase(it, str.end());
			break;
		}
	}
}

/*
** Local Variables:
** c-basic-offset: 3
** tab-width: 3
** End:
** vim:
** vim: noet ts=3 sw=3
*/
