/*	$Id: SNPPClient.c++ 1077 2012-01-23 16:43:09Z faxguy $ */
/*
 * Copyright (c) 1995-1996 Sam Leffler
 * Copyright (c) 1995-1996 Silicon Graphics, Inc.
 * HylaFAX is a trademark of Silicon Graphics
 *
 * Permission to use, copy, modify, distribute, and sell this software and 
 * its documentation for any purpose is hereby granted without fee, provided
 * that (i) the above copyright notices and this permission notice appear in
 * all copies of the software and related documentation, and (ii) the names of
 * Sam Leffler and Silicon Graphics may not be used in any advertising or
 * publicity relating to the software without the specific, prior written
 * permission of Sam Leffler and Silicon Graphics.
 * 
 * THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, 
 * EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY 
 * WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.  
 * 
 * IN NO EVENT SHALL SAM LEFFLER OR SILICON GRAPHICS BE LIABLE FOR
 * ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL DAMAGES OF ANY KIND,
 * OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
 * WHETHER OR NOT ADVISED OF THE POSSIBILITY OF DAMAGE, AND ON ANY THEORY OF 
 * LIABILITY, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE 
 * OF THIS SOFTWARE.
 */
#include "config.h"
#include "Sys.h"
#include "SNPPClient.h"
#include <pwd.h>
#include <ctype.h>
#include <sys/types.h>
#include <errno.h>

#define	N(a)	(sizeof (a) / sizeof (a[0]))

SNPPClient::SNPPClient()
{
    init();
}

SNPPClient::SNPPClient(const fxStr& hostarg)
{
    init();
    setupHostModem(hostarg);
}

SNPPClient::SNPPClient(const char* hostarg)
{
    init();
    setupHostModem(hostarg);
}

void
SNPPClient::init()
{
    jobs = new SNPPJobArray;
    fdIn = NULL;
    fdOut = NULL;
    state = 0;
    msg = NULL;
    pasv = false;

    setupConfig();
}

void
SNPPClient::initServerState(void)
{
}

SNPPClient::~SNPPClient()
{
    (void) hangupServer();
    delete jobs;
    delete msg;
}

void
SNPPClient::printError(const char* fmt ...)
{
    va_list ap;
    va_start(ap, fmt);
    vprintError(fmt, ap);
    va_end(ap);
}
void
SNPPClient::vprintError(const char* fmt, va_list ap)
{
    vfprintf(stderr, fmt, ap);
    fputs("\n", stderr);
}

void
SNPPClient::printWarning(const char* fmt ...)
{
    va_list ap;
    va_start(ap, fmt);
    vprintWarning(fmt, ap);
    va_end(ap);
}
void
SNPPClient::vprintWarning(const char* fmt, va_list ap)
{
    fprintf(stderr, "%s", "Warning, ");
    vfprintf(stderr, fmt, ap);
    fputs("\n", stderr);
}

void
SNPPClient::traceServer(const char* fmt ...)
{
    va_list ap;
    va_start(ap, fmt);
    vtraceServer(fmt, ap);
    va_end(ap);
}
void
SNPPClient::vtraceServer(const char* fmt, va_list ap)
{
    vfprintf(stdout, fmt, ap);
    fputs("\n", stdout);
}

/*
 * Host, port, and modem can be specified using
 *
 *     modem@host:port
 *
 * e.g. ttyf2@flake.asd:9999.  Alternate forms
 * are: modem@, modem@host, host, host:port.
 */
void
SNPPClient::setupHostModem(const fxStr& s)
{
    u_int pos = s.next(0, '@');
    if (pos != s.length()) {
	modem = s.head(pos);
	host = s.tail(s.length() - (pos+1));
    } else
	host = s;
    pos = host.next(0, ':');
    if (pos != host.length()) {
	port = atoi(host.tail(host.length() - (pos+1)));
	host.resize(pos);
    }
}
void SNPPClient::setupHostModem(const char* cp) { setupHostModem(fxStr(cp)); }

void SNPPClient::setHost(const fxStr& hostarg)	{ setupHostModem(hostarg); }
void SNPPClient::setHost(const char* hostarg)	{ setupHostModem(hostarg); }
void SNPPClient::setPort(int p)			{ port = p; }
void SNPPClient::setProtoName(const char* s)	{ proto = s; }

void SNPPClient::setModem(const fxStr& modemarg){ modem = modemarg; }
void SNPPClient::setModem(const char* modemarg)	{ modem = modemarg; }

void
SNPPClient::setVerbose(bool v)
{
    if (v)
	state |= SS_VERBOSE;
    else
	state &= ~SS_VERBOSE;
}

/*
 * Setup the sender's identity.
 */
bool
SNPPClient::setupSenderIdentity(fxStr& emsg)
{
    setupUserIdentity(emsg);			// client identity

    if (from != "") {
	u_int l = from.next(0, '<');
	if (l == from.length()) {
	    l = from.next(0, '(');
	    if (l != from.length()) {		// joe@foobar (Joe Schmo)
		setBlankMailboxes(from.head(l));
		l++, senderName = from.token(l, ')');
	    } else {				// joe
		setBlankMailboxes(from);
		if (from != getUserName())
		    senderName = "";
	    }
	} else {				// Joe Schmo <joe@foobar>
	    senderName = from.head(l);
	    l++, setBlankMailboxes(from.token(l, '>'));
	}
	if (senderName == "" && getNonBlankMailbox(senderName)) {
	    /*
	     * Mail address, but no "real name"; construct one from
	     * the account name.  Do this by first stripping anything
	     * to the right of an '@' and then stripping any leading
	     * uucp patch (host!host!...!user).
	     */
	    senderName.resize(senderName.next(0, '@'));
	    senderName.remove(0, senderName.nextR(senderName.length(), '!'));
	}

	// strip and leading&trailing white space
	senderName.remove(0, senderName.skip(0, " \t"));
	senderName.resize(senderName.skipR(senderName.length(), " \t"));
    } else {
	setBlankMailboxes(getUserName());
    }
    fxStr mbox;
    if (senderName == "" || !getNonBlankMailbox(mbox)) {
	emsg = "Malformed (null) sender name or mail address";
	return (false);
    } else
	return (true);
}
void SNPPClient::setFromIdentity(const char* s)		{ from = s; }

/*
 * Assign the specified string to any unspecified email
 * addresses used for notification mail.
 */
void
SNPPClient::setBlankMailboxes(const fxStr& s)
{
    for (u_int i = 0, n = jobs->length(); i < n; i++) {
	SNPPJob& job = (*jobs)[i];
	if (job.getMailbox() == "")
	    job.setMailbox(s);
    }
}

/*
 * Return the first non-null mailbox string
 * in the set of jobs.
 */
bool
SNPPClient::getNonBlankMailbox(fxStr& s)
{
    for (u_int i = 0, n = jobs->length(); i < n; i++) {
	SNPPJob& job = (*jobs)[i];
	if (job.getMailbox() != "") {
	    s = job.getMailbox();
	    return (true);
	}
    }
    return (false);
}

bool
SNPPClient::setupUserIdentity(fxStr& emsg)
{
    struct passwd* pwd;

    pwd = getpwuid(getuid());
    if (!pwd) {
	emsg = fxStr::format(
	    "Can not locate your password entry (uid %lu): %s.",
		(u_long) getuid(), strerror(errno));
	return (false);
    }
    userName = pwd->pw_name;
    if (pwd->pw_gecos && pwd->pw_gecos[0] != '\0') {
	senderName = pwd->pw_gecos;
	senderName.resize(senderName.next(0, '('));	// strip SysV junk
	u_int l = senderName.next(0, '&');
	if (l < senderName.length()) {
	    /*
	     * Do the '&' substitution and raise the
	     * case of the first letter of the inserted
	     * string (the usual convention...)
	     */
	    senderName.remove(l);
	    senderName.insert(userName, l);
	    if (islower(senderName[l]))
		senderName[l] = toupper(senderName[l]);
	}
	senderName.resize(senderName.next(0,','));
    } else
	senderName = userName;
    if (senderName.length() == 0) {
	emsg = "Bad (null) user name; your password file entry"
	    " probably has bogus GECOS field information.";
	return (false);
    } else
	return (true);
}

void
SNPPClient::setPagerMsg(const char* v)
{
    delete msg;
    msg = new fxStr(v);
    msgFile ="";
}

void
SNPPClient::setPagerMsgFile(const char* v)
{
    msgFile = v;
    delete msg;
}

/*
 * Configuration file support.
 */

SNPPClient::S_stringtag SNPPClient::strings[] = {
{ "protocol",			&SNPPClient::proto,	SNPP_PROTONAME },
{ "host",			&SNPPClient::host,	NULL },
{ "modem",			&SNPPClient::modem,	NULL },
};
SNPPClient::S_numbertag SNPPClient::numbers[] = {
{ "port",			&SNPPClient::port,	(u_int) -1 },
};

/*
 * Configuration file support.
 */
#define	N(a)	(sizeof (a) / sizeof (a[0]))

void
SNPPClient::setupConfig()
{
    int i;

    for (i = N(strings)-1; i >= 0; i--)
	(*this).*strings[i].p = (strings[i].def ? strings[i].def : "");
    for (i = N(numbers)-1; i >= 0; i--)
	(*this).*numbers[i].p = numbers[i].def;
    initServerState();

    jproto.setQueued(SNPP_DEFQUEUE);
    jproto.setNotification(SNPP_DEFNOTIFY);
    jproto.setHoldTime(0);		// immediate delivery
    jproto.setRetryTime((u_int) -1);
    jproto.setMaxTries(SNPP_DEFRETRIES);
    jproto.setMaxDials(SNPP_DEFREDIALS);
    jproto.setServiceLevel(SNPP_DEFLEVEL);
    jproto.setMailbox("");
}

void
SNPPClient::resetConfig()
{
    setupConfig();
}

void
SNPPClient::configError(const char* fmt ...)
{
    va_list ap;
    va_start(ap, fmt);
    vprintError(fmt, ap);
    va_end(ap);
}

void
SNPPClient::configTrace(const char* fmt ...)
{
    if (getVerbose()) {
	va_list ap;
	va_start(ap, fmt);
	vprintWarning(fmt, ap);
	va_end(ap);
    }
}

bool
SNPPClient::setConfigItem(const char* tag, const char* value)
{
    u_int ix;
    if (findTag(tag, (const tags*) strings, N(strings), ix)) {
	(*this).*strings[ix].p = value;
    } else if (findTag(tag, (const tags*) numbers, N(numbers), ix)) {
	(*this).*numbers[ix].p = getNumber(value);
    } else if (streq(tag, "verbose")) {
	if (getBoolean(value))
	    state |= SS_VERBOSE;
	else
	    state &= ~SS_VERBOSE;
    } else if (streq(tag, "queuesend")) {
	jproto.setQueued(getBoolean(value));
    } else if (streq(tag, "notify") || streq(tag, "notification")) {
	jproto.setNotification(value);
    } else if (streq(tag, "holdtime")) {
	fxStr emsg;
	if (!jproto.setHoldTime(tag, emsg))
	    configError("Invalid hold time \"%s\": %s",
		value, (const char*) emsg);
    } else if (streq(tag, "retrytime")) {
	jproto.setRetryTime(value);
    } else if (streq(tag, "maxtries")) {
	jproto.setMaxTries(getNumber(value));
    } else if (streq(tag, "maxdials")) {
	jproto.setMaxDials(getNumber(value));
    } else if (streq(tag, "servicelevel")) {
	jproto.setServiceLevel(getNumber(value));
    } else if (streq(tag, "mailaddr")) {
	jproto.setMailbox(value);
    } else if (streq(tag, "passivemode")) {
	pasv = getBoolean(value);
    } else
	return (false);
    return (true);
}

bool
SNPPClient::callServer(fxStr& emsg)
{
    if (host.length() == 0) {		// if host not specified by -h
	const char* cp = getenv("SNPPSERVER");
	if (cp && *cp != '\0') {
	    if (modem != "") {		// don't clobber specified modem
		fxStr m(modem);
		setupHostModem(cp);
		modem = m;
	    } else
		setupHostModem(cp);
	} else				// use default host
	    host = SNPP_DEFHOST;
    }
    if (callInetServer(emsg)) {
	signal(SIGPIPE, fxSIGHANDLER(SIG_IGN));
	/*
	 * Transport code is expected to call back through
	 * setCtrlFds so fdIn should be properly setup...
	 */
	return (fdIn != NULL && getReply(false) == COMPLETE);
    } else
	return (false);
}

#if CONFIG_INETTRANSPORT
#include "Socket.h"

extern "C" {
#include <arpa/inet.h>
#include <arpa/telnet.h>
#include <netinet/in.h>
#include <netinet/in_systm.h>
#include <netinet/ip.h>
#include <netdb.h>
}

bool
SNPPClient::callInetServer(fxStr& emsg)
{
    fxStr proto(getProtoName());
    char* cp;
    if ((cp = getenv("SNPPSERVICE")) && *cp != '\0') {
	fxStr s(cp);
	u_int l = s.next(0,'/');
	port = (u_int) (int) s.head(l);
	if (l < s.length())
	    proto = s.tail(s.length()-(l+1));
    }
    struct hostent* hp = Socket::gethostbyname(getHost());
    if (!hp) {
	emsg = getHost() | ": Unknown host";
	return (false);
    }
    int protocol;
    const char* cproto = proto;			// XXX for busted include files
    struct protoent* pp = getprotobyname(cproto);
    if (!pp) {
	printWarning("%s: No protocol definition, using default.", cproto);
	protocol = 0;
    } else
	protocol = pp->p_proto;
    int fd = socket(hp->h_addrtype, SOCK_STREAM, protocol);
    if (fd < 0) {
	emsg = "Can not create socket to connect to server.";
	return (false);
    }
    struct sockaddr_in sin;
    memset(&sin, 0, sizeof (sin));
    sin.sin_family = hp->h_addrtype;
    if (port == (u_int) -1) {
	struct servent* sp = getservbyname(SNPP_SERVICE, cproto);
	if (!sp) {
	    if (!isdigit(cproto[0])) {
		printWarning(
		    "No \"%s\" service definition, using default %u/%s.",
		    SNPP_SERVICE, SNPP_DEFPORT, cproto);
		sin.sin_port = htons(SNPP_DEFPORT);
	    } else
		sin.sin_port = htons(atoi(cproto));
	} else
	    sin.sin_port = sp->s_port;
    } else
	sin.sin_port = htons(port);
    for (char** cpp = hp->h_addr_list; *cpp; cpp++) {
	memcpy(&sin.sin_addr, *cpp, hp->h_length);
	if (getVerbose())
	    traceServer("Trying %s (%s) at port %u...",
		(const char*) getHost(),
		inet_ntoa(sin.sin_addr),
		ntohs(sin.sin_port));
	if (Socket::connect(fd, &sin, sizeof (sin)) >= 0) {
	    if (getVerbose())
		traceServer("Connected to %s.", hp->h_name);
#if defined(IP_TOS) && defined(IPTOS_LOWDELAY)
	    int tos = IPTOS_LOWDELAY;
	    if (Socket::setsockopt(fd, IPPROTO_IP, IP_TOS, &tos, sizeof (tos)) < 0)
		printWarning("setsockopt(TOS): %s (ignored)",
		    strerror(errno));
#endif
#ifdef SO_OOBINLINE
	    int on = 1;
	    if (Socket::setsockopt(fd, SOL_SOCKET, SO_OOBINLINE, &on, sizeof (on)) < 0)
		printWarning("setsockopt(OOBLINE): %s (ignored)",
		    strerror(errno));
#endif
	    setCtrlFds(fd, fd);
	    return (true);
	}
    }
    emsg = fxStr::format("Can not reach server at host \"%s\", port %u.",
	(const char*) getHost(), ntohs(sin.sin_port));
    close(fd), fd = -1;
    return (false);
}
#else
bool
SNPPServer::callInetServer(fxStr& emsg)
{
    emsg = "Sorry, no TCP/IP communication support was configured.";
    return (false);
}
#endif

bool
SNPPClient::hangupServer(void)
{
    if (fdIn != NULL)
	fclose(fdIn), fdIn = NULL;
    if (fdOut != NULL)
	fclose(fdOut), fdOut = NULL;
    initServerState();
    return (true);
}

void
SNPPClient::setCtrlFds(int in, int out)
{
    if (fdIn != NULL)
	fclose(fdIn);
    fdIn = fdopen(in, "r");
    if (fdOut != NULL)
	fclose(fdOut);
    fdOut = fdopen(out, "w");
}

/*
 * Do login procedure.
 */
bool
SNPPClient::login(const char* user, const char* pass, fxStr& emsg)
{
    if (user == NULL) {
	setupSenderIdentity(emsg);		// invokes setupUserIdentity
	user = userName;
    }
    int n = command("LOGI %s", user);
    if (code == 550)
	n = command("LOGI %s %s", user, pass ? pass : getPasswd("Password:"));
    if (n == COMPLETE)
	state |= SS_LOGGEDIN;
    else
	state &= ~SS_LOGGEDIN;
    if (isLoggedIn()) {
	if (command("SITE HELP NOTIFY") == COMPLETE)
	    state |= SS_HASSITE;
	else
	    state &= ~SS_HASSITE;
	return (true);
    } else {
	emsg = "Login failed: " | lastResponse;
	return (false);
    }
}

/*
 * Prompt for a password.
 */
const char*
SNPPClient::getPasswd(const char* prompt)
{
    return (getpass(prompt));
}

void
SNPPClient::lostServer(void)
{
    printError("Service not available, remote server closed connection");
    hangupServer();
}

void
SNPPClient::unexpectedResponse(fxStr& emsg)
{
    emsg = "Unexpected server response: " | lastResponse;
}

void
SNPPClient::protocolBotch(fxStr& emsg, const char* fmt ...)
{
    va_list ap;
    va_start(ap, fmt);
    emsg = "Protocol botch" | fxStr::vformat(fmt, ap);
    va_end(ap);
}

/*
 * Send a command and wait for a response.
 * The primary response code is returned.
 */
int
SNPPClient::command(const char* fmt ...)
{
    va_list ap1, ap2;
    va_start(ap1, fmt);
    va_start(ap2, fmt);
    int r = vcommand(fmt, ap1, ap2);
    va_end(ap1);
    va_end(ap2);
    return (r);
}

/*
 * Send a command and wait for a response.
 * The primary response code is returned.
 */
int
SNPPClient::vcommand(const char* fmt, va_list ap1, va_list ap2)
{
    if (getVerbose()) {
	if (strncasecmp("LOGI", fmt, 4) == 0)
	    traceServer("-> LOGI XXXX");
	else {
        fxStr f("-> ");
        f.append(fmt);
	    vtraceServer(f, ap1);
	}
    }
    if (fdOut == NULL) {
	printError("No control connection for command");
	code = -1;
	return (0);
    }
    vfprintf(fdOut, fmt, ap2);
    fputs("\r\n", fdOut);
    (void) fflush(fdOut);
    int r = getReply(strncmp(fmt, "QUIT", 4) == 0);
    return (r);
}

/*
 * Extract a valid reply code from a string.
 * The code must be 3 numeric digits followed
 * by a space or ``-'' (the latter indicates
 * the server reponse is to be continued with
 * one or more lines).  If no valid reply code
 * is recognized zero is returned--this is
 * assumed to be an invalid reply code.
 */
static int
getReplyCode(const char* cp)
{
    if (!isdigit(cp[0]))
	return (0);
    int c = (cp[0] - '0');
    if (!isdigit(cp[1]))
	return (0);
    c = 10*c + (cp[1]-'0');
    if (!isdigit(cp[2]))
	return (0);
    c = 10*c + (cp[2]-'0');
    return ((cp[3] == ' ' || cp[3] == '-') ? c : 0);
}

/*
 * Read from the control channel until a valid reply is
 * received or the connection is dropped.  The last line
 * of the received response is left in SNPPClient::lastResponse
 * and the reply code is left in SNPPClient::code.  The
 * primary response (the first digit of the reply code)
 * is returned to the caller.  Continuation lines are
 * handled but not collected.
 */
int
SNPPClient::getReply(bool expecteof)
{
    int firstCode = 0;
    bool continuation = false;
    do {
	lastResponse.resize(0);
	int c;
	while ((c = getc(fdIn)) != '\n') {
	    if (c == IAC) {     // handle telnet commands
		switch (c = getc(fdIn)) {
		case WILL:
		case WONT:
		    c = getc(fdIn);
		    fprintf(fdOut, "%c%c%c", IAC, DONT, c);
		    (void) fflush(fdOut);
		    break;
		case DO:
		case DONT:
		    c = getc(fdIn);
		    fprintf(fdOut, "%c%c%c", IAC, WONT, c);
		    (void) fflush(fdOut);
		    break;
		default:
		    break;
		}
		continue;
	    }
	    if (c == EOF) {
		if (expecteof) {
		    code = 221;
		    return (0);
		} else {
		    lostServer();
		    code = 421;
		    return (4);
		}
	    }
	    if (c != '\r')
		lastResponse.append(c);
	}
	if (getVerbose())
	    traceServer("%s", (const char*) lastResponse);
	code = getReplyCode(lastResponse);
	if (code != 0) {			// found valid reply code
	    if (lastResponse[3] == '-') {	// continuation line
		if (firstCode == 0)		// first line of reponse
		    firstCode = code;
		continuation = true;
	    } else if (code == firstCode)	// end of continued reply
		continuation = false;
	}
    } while (continuation || code == 0);

    if (code == 421)				// server closed connection
	lostServer();
    return (code/100);
}

/*
 * Extract a string from a reply message.  The
 * string that is extracted is expected to follow
 * a pattern string.  The pattern is tried both
 * in the initial case and then the inverse case
 * (upper or lower depending on what the original
 * case was).  The resulting string is checked to
 * make sure that it is not null.
 */
bool
SNPPClient::extract(u_int& pos, const char* pattern, fxStr& result)
{
    fxStr pat(pattern);
    u_int l = lastResponse.find(pos, pat);
    if (l == lastResponse.length()) {		// try inverse-case version
	if (isupper(pattern[0]))
	    pat.lowercase();
	else
	    pat.raisecase();
	l = lastResponse.find(pos, pat);
    }
    if (l == lastResponse.length())
	return (false);
    l = lastResponse.skip(l+pat.length(), ' ');// skip white space
    result = lastResponse.extract(l, lastResponse.next(l, ' ')-l);
    if (result == "")
	return (false);
    pos = l;					// update position
    return (true);
}

/*
 * Create a new job and return its job-id if
 * parsed from the reply (this is for HylaFAX,
 * RFC 1861 says nothing about this).
 */
bool
SNPPClient::newPage(const fxStr& pin, const fxStr& passwd, fxStr& jobid, fxStr& emsg)
{
    int result;
    if (passwd != "")
	result = command("PAGE %s %s", (const char*) pin, (const char*) passwd);
    else
	result = command("PAGE %s", (const char*) pin);
    if (result == COMPLETE) {
	if (code == 250) {
	    /*
	     * If the server is hfaxd, then the response should be
	     * of the form:
	     *
	     * 250 ... jobid: xxxx.
	     *
	     * where xxxx is the ID for the new job.
	     */
	    u_int l = 0;
	    if (extract(l, "jobid:", jobid)) {
		/*
		 * Force job IDs to be numeric;
		 * this deals with servers that want to append
		 * punctuation such as ``,'' or ``.''.
		 */
		jobid.resize(jobid.skip(0, "0123456789"));
	    } else
		jobid = "unknown";
	    return (true);
	} else
	    unexpectedResponse(emsg);
    } else
	emsg = lastResponse;
    return (false);
}

SNPPJob&
SNPPClient::addJob(void)
{
    u_int ix = jobs->length();
    jobs->resize(ix+1);
    (*jobs)[ix] = jproto;
    return ((*jobs)[ix]);
}
u_int SNPPClient::getNumberOfJobs() const	{ return jobs->length(); }

SNPPJob*
SNPPClient::findJob(const fxStr& pin)
{
    for (u_int i = 0, n = jobs->length(); i < n; i++) {
	SNPPJob& job = (*jobs)[i];
	if (job.getPIN() == pin)
	    return (&job);
    }
    return (NULL);
}

void
SNPPClient::removeJob(const SNPPJob& job)
{
    u_int ix = jobs->find(job);
    if (ix != fx_invalidArrayIndex)
	jobs->remove(ix);
}

bool
SNPPClient::prepareForJobSubmissions(fxStr&)
{
    // XXX nothing to do right now
    return (true);
}

bool
SNPPClient::submitJobs(fxStr& emsg)
{
    if (!isLoggedIn()) {
	emsg = "Not logged in to server";
	return (false);
    }
    /*
     * Construct jobs and submit them.
     */
    for (u_int i = 0, n = jobs->length(); i < n; i++) {
	SNPPJob& job = (*jobs)[i];
	if (!job.createJob(*this, emsg))
	    return (false);
	notifyNewJob(job);			// notify client
    }
    if (msgFile != "") {
	if (!sendData(msgFile, emsg))
	    return (false);
    } else if (msg) {
	if (!sendMsg(*msg, emsg))
	    return (false);
    }
    if (command("SEND") != COMPLETE) {
	emsg = lastResponse;
	return (false);
    } else
	return (true);
}

/*
 * Default notification handler for when a new job is created.
 */
void
SNPPClient::notifyNewJob(const SNPPJob& job)
{
    printf("destination pin %s: request id is %s for host %s\n"
	, (const char*) job.getPIN()
	, (const char*) job.getJobID()
	, (const char*) getHost()
    );
}

/*
 * Send a block of raw data on the data
 * conenction, interpreting write errors.
 */
bool
SNPPClient::sendRawData(void* buf, int cc, fxStr& emsg)
{
#ifdef __linux__
    /*
     * Linux kernel bug: can get short writes on
     * stream sockets when setup for blocking i/o.
     */
    u_char* bp = (u_char*) buf;
    for (int cnt, sent = 0; cc; sent += cnt, cc -= cnt) 
	if ((cnt = write(fileno(fdOut), bp + sent, cc)) <= 0) {
	    protocolBotch(emsg, errno == EPIPE ?
		" (server closed connection)" : " (server write error: %s).",
		strerror(errno));
	    return (false);
	}
#else
    if (write(fileno(fdOut), buf, cc) != cc) {
	protocolBotch(emsg, errno == EPIPE ?
	    " (server closed connection)" : " (server write error: %s).",
	    strerror(errno));
	return (false);
    }
#endif
    return (true);
}

bool
SNPPClient::sendData(int fd, fxStr& emsg)
{
    struct stat sb;
    (void) Sys::fstat(fd, sb);
    if (getVerbose())
	traceServer("SEND message data, %lu bytes", (u_long) sb.st_size);
    if (command("DATA") == CONTINUE) {
	size_t cc = (size_t) sb.st_size;
	while (cc > 0) {
	    char buf[32*1024];
	    size_t n = fxmin(cc, sizeof (buf));
	    if (read(fd, buf, n) != (ssize_t) n) {
		protocolBotch(emsg, " (data read: %s).", strerror(errno));
		return (false);
	    }
	    if (!sendRawData(buf, n, emsg))
		return (false);
	    cc -= n;
	}
	if (command(".") == COMPLETE)
	    return (true);
    }
    emsg = getLastResponse();
    return (false);
}

bool
SNPPClient::sendData(const fxStr& filename, fxStr& emsg)
{
    bool ok = false;
    int fd = Sys::open(filename, O_RDONLY);
    if (fd >= 0) {
	ok = sendData(fd, emsg);
	Sys::close(fd);
    } else
	emsg = fxStr::format("Unable to open message file \"%s\".",
	    (const char*) filename);
    return (ok);
}

bool
SNPPClient::sendMsg(const char* msg, fxStr& emsg)
{
    if (command("MESS %s", msg) != COMPLETE) {
	emsg = getLastResponse();
	return (false);
    } else
	return (true);
}

bool
SNPPClient::siteParm(const char* name, const fxStr& value)
{
    if (!hasSiteCmd()) {
	printWarning("no SITE %s support; ignoring set request.", name);
	return (true);
    } else
	return (command("SITE %s %s", name, (const char*) value) == COMPLETE);
}

bool
SNPPClient::siteParm(const char* name, u_int value)
{
    if (!hasSiteCmd()) {
	printWarning("no SITE %s support; ignoring set request.", name);
	return (true);
    } else
	return (command("SITE %s %u", name, value) == COMPLETE);
}

bool
SNPPClient::setHoldTime(u_int t)
{
    time_t tv = t;
    struct tm* tm = gmtime(&tv);
    return (command("HOLD %02d%02d%02d%02d%02d"
	, (tm->tm_year) % 100
	, tm->tm_mon+1
	, tm->tm_mday
	, tm->tm_hour
	, tm->tm_min) == COMPLETE);
}

bool
SNPPClient::setRetryTime(u_int t)
{
    return siteParm("RETRYTIME", fxStr::format("%02d%02d", t/60, t%60));
}
