/*
Copyright (C) 2004 IC & S dbmail@ic-s.nl
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., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
/* $Id: lmtp.c 2199 2006-07-18 11:07:53Z paul $
*
* implementation for lmtp commands according to RFC 1081 */
#include "dbmail.h"
#define INCOMING_BUFFER_SIZE 512
/* default timeout for server daemon */
#define DEFAULT_SERVER_TIMEOUT 300
/* max_errors defines the maximum number of allowed failures */
#define MAX_ERRORS 3
/* max_in_buffer defines the maximum number of bytes that are allowed to be
* in the incoming buffer */
#define MAX_IN_BUFFER 255
extern volatile sig_atomic_t alarm_occured;
/* These are needed across multiple calls to lmtp() */
static struct dm_list from, rcpt;
/* allowed lmtp commands */
static const char *const commands[] = {
"LHLO", "QUIT", "RSET", "DATA", "MAIL",
"VRFY", "EXPN", "HELP", "NOOP", "RCPT"
};
static const char validchars[] =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
"_.!|@#$%^&*()-+=~[]{}<>:;\\/ ";
static char myhostname[64];
/**
* \function lmtp_error
*
* report an LMTP error
* \param session current LMTP session
* \param stream stream to right to
* \param formatstring format string
* \param ... values to fill up formatstring
*/
int lmtp_error(PopSession_t * session, void *stream,
const char *formatstring, ...) PRINTF_ARGS(3, 4);
/**
* initialize a new session. Sets all relevant variables in session
* \param[in,out] session to initialize
*/
void lmtp_init(PopSession_t *session)
{
/* setting Session variables */
session->state = STRT;
session->error_count = 0;
session->username = NULL;
session->password = NULL;
session->SessionResult = 0;
/* reset counters */
session->totalsize = 0;
session->virtual_totalsize = 0;
session->totalmessages = 0;
session->virtual_totalmessages = 0;
/* set the lists to zero length */
dm_list_init(&rcpt);
dm_list_init(&from);
}
int lmtp_reset(PopSession_t * session)
{
if (dm_list_length(&rcpt) > 0) {
dsnuser_free_list(&rcpt);
}
dm_list_init(&rcpt);
if (dm_list_length(&from) > 0) {
dm_list_free(&from.start);
}
dm_list_init(&from);
session->state = LHLO;
return 1;
}
int lmtp_handle_connection(clientinfo_t * ci)
{
/*
Handles connection and calls
lmtp command handler
*/
int done = 1; /* loop state */
char *buffer = NULL; /* connection buffer */
int cnt; /* counter */
PopSession_t session; /* current connection session */
lmtp_init(&session);
/* getting hostname */
gethostname(myhostname, 64);
myhostname[63] = 0; /* make sure string is terminated */
buffer = (char *) dm_malloc(INCOMING_BUFFER_SIZE * sizeof(char));
if (!buffer) {
trace(TRACE_MESSAGE,
"lmtp_handle_connection(): Could not allocate buffer");
return 0;
}
if (ci->tx) {
/* sending greeting */
ci_write(ci->tx,
"220 %s DBMail LMTP service ready to rock\r\n",
myhostname);
fflush(ci->tx);
} else {
trace(TRACE_MESSAGE,
"lmtp_handle_connection(): TX stream is null!");
dm_free(buffer);
return 0;
}
lmtp_reset(&session);
while (done > 0) {
if (db_check_connection()) {
trace(TRACE_DEBUG,"%s,%s: database has gone away", __FILE__, __func__);
done=-1;
break;
}
/* set the timeout counter */
alarm(ci->timeout);
/* clear the buffer */
memset(buffer, 0, INCOMING_BUFFER_SIZE);
for (cnt = 0; cnt < INCOMING_BUFFER_SIZE - 1; cnt++) {
do {
clearerr(ci->rx);
fread(&buffer[cnt], 1, 1, ci->rx);
/* leave, an alarm has occured during fread */
if (alarm_occured) {
alarm_occured = 0;
client_close();
dm_free(buffer);
return 0;
}
} while (ferror(ci->rx) && errno == EINTR);
if (buffer[cnt] == '\n' || feof(ci->rx)
|| ferror(ci->rx)) {
buffer[cnt + 1] = '\0';
break;
}
}
if (feof(ci->rx) || ferror(ci->rx)) {
/* check client eof */
done = -1;
} else {
/* reset function handle timeout */
alarm(0);
/* handle lmtp commands */
done =
lmtp(ci->tx, ci->rx, buffer, ci->ip_src, &session);
}
fflush(ci->tx);
}
/* memory cleanup */
lmtp_reset(&session);
dm_free(buffer);
buffer = NULL;
/* reset timers */
alarm(0);
return 0;
}
int lmtp_error(PopSession_t * session, void *stream,
const char *formatstring, ...)
{
va_list argp;
if (session->error_count >= MAX_ERRORS) {
trace(TRACE_MESSAGE,
"lmtp_error(): too many errors (MAX_ERRORS is %d)",
MAX_ERRORS);
ci_write((FILE *) stream,
"500 Too many errors, closing connection.\r\n");
session->SessionResult = 2; /* possible flood */
lmtp_reset(session);
return -3;
} else {
va_start(argp, formatstring);
if (vfprintf((FILE *) stream, formatstring, argp) < 0) {
va_end(argp);
trace(TRACE_ERROR, "%s,%s: error writing to stream",
__FILE__, __func__);
return -1;
}
va_end(argp);
}
trace(TRACE_DEBUG, "lmtp_error(): an invalid command was issued");
session->error_count++;
return 1;
}
int lmtp(void *stream, void *instream, char *buffer,
char *client_ip UNUSED, PopSession_t * session)
{
/* returns values:
* 0 to quit
* -1 on failure
* 1 on success */
char *command, *value;
int cmdtype;
int indx = 0;
/* buffer overflow attempt */
if (strlen(buffer) > MAX_IN_BUFFER) {
trace(TRACE_DEBUG, "lmtp(): buffer overflow attempt");
return -3;
}
/* check for command issued */
while (strchr(validchars, buffer[indx]))
indx++;
/* end buffer */
buffer[indx] = '\0';
trace(TRACE_DEBUG, "lmtp(): incoming buffer: [%s]", buffer);
command = buffer;
value = strstr(command, " "); /* look for the separator */
if (value != NULL) {
*value = '\0'; /* set a \0 on the command end */
value++; /* skip space */
if (strlen(value) == 0) {
value = NULL; /* no value specified */
} else {
trace(TRACE_DEBUG,
"lmtp(): command issued :cmd [%s], value [%s]\n",
command, value);
}
}
for (cmdtype = LMTP_STRT; cmdtype < LMTP_END; cmdtype++)
if (strcasecmp(command, commands[cmdtype]) == 0)
break;
trace(TRACE_DEBUG, "lmtp(): command looked up as commandtype %d",
cmdtype);
/* commands that are allowed to have no arguments */
if ((value == NULL) &&
!((cmdtype == LMTP_LHLO) || (cmdtype == LMTP_DATA) ||
(cmdtype == LMTP_RSET) || (cmdtype == LMTP_QUIT) ||
(cmdtype == LMTP_NOOP) || (cmdtype == LMTP_HELP) )) {
trace(TRACE_ERROR, "ARGUMENT %d", cmdtype);
return lmtp_error(session, stream,
"500 This command requires an argument.\r\n");
}
switch (cmdtype) {
case LMTP_QUIT:
{
ci_write((FILE *) stream, "221 %s BYE\r\n",
myhostname);
lmtp_reset(session);
return 0; /* return 0 to cause the connection to close */
}
case LMTP_NOOP:
{
ci_write((FILE *) stream, "250 OK\r\n");
return 1;
}
case LMTP_RSET:
{
ci_write((FILE *) stream, "250 OK\r\n");
lmtp_reset(session);
return 1;
}
case LMTP_LHLO:
{
/* Reply wth our hostname and a list of features.
* The RFC requires a couple of SMTP extensions
* with a MUST statement, so just hardcode them.
* */
ci_write((FILE *) stream,
"250-%s\r\n"
"250-PIPELINING\r\n"
"250-ENHANCEDSTATUSCODES\r\n"
/* This is a SHOULD implement:
* "250-8BITMIME\r\n"
* Might as well do these, too:
* "250-CHUNKING\r\n"
* "250-BINARYMIME\r\n"
* */
"250 SIZE\r\n", myhostname);
lmtp_reset(session);
return 1;
}
case LMTP_HELP:
{
int helpcmd;
if (value == NULL)
helpcmd = LMTP_END;
else
for (helpcmd = LMTP_STRT;
helpcmd < LMTP_END; helpcmd++)
if (strcasecmp
(value,
commands[helpcmd]) == 0)
break;
trace(TRACE_DEBUG,
"lmtp(): LMTP_HELP requested for commandtype %d",
helpcmd);
if ((helpcmd == LMTP_LHLO)
|| (helpcmd == LMTP_DATA)
|| (helpcmd == LMTP_RSET)
|| (helpcmd == LMTP_QUIT)
|| (helpcmd == LMTP_NOOP)
|| (helpcmd == LMTP_HELP)) {
ci_write((FILE *) stream, "%s",
LMTP_HELP_TEXT[helpcmd]);
} else {
ci_write((FILE *) stream, "%s",
LMTP_HELP_TEXT[LMTP_END]);
}
return 1;
}
case LMTP_VRFY:
{
/* RFC 2821 says this SHOULD be implemented...
* and the goal is to say if the given address
* is a valid delivery address at this server. */
ci_write((FILE *) stream,
"502 Command not implemented\r\n");
return 1;
}
case LMTP_EXPN:
{
/* RFC 2821 says this SHOULD be implemented...
* and the goal is to return the membership
* of the specified mailing list. */
ci_write((FILE *) stream,
"502 Command not implemented\r\n");
return 1;
}
case LMTP_MAIL:
{
/* We need to LHLO first because the client
* needs to know what extensions we support.
* */
if (session->state != LHLO) {
ci_write((FILE *) stream,
"550 Command out of sequence.\r\n");
} else if (dm_list_length(&from) > 0) {
ci_write((FILE *) stream,
"500 Sender already received. Use RSET to clear.\r\n");
trace(TRACE_ERROR, "%s,%s: Sender already received: %s",
__FILE__, __func__, (char *)(dm_list_getstart(&from)->data));
} else {
/* First look for an email address.
* Don't bother verifying or whatever,
* just find something between angle brackets!
* */
int goodtogo = 1;
size_t tmplen = 0, tmppos = 0;
char *tmpaddr = NULL, *tmpbody = NULL;
find_bounded(value, '<', '>', &tmpaddr,
&tmplen, &tmppos);
/* Second look for a BODY keyword.
* See if it has an argument, and if we
* support that feature. Don't give an OK
* if we can't handle it yet, like 8BIT!
* */
/* Find the '=' following the address
* then advance one character past it
* (but only if there's more string!)
* */
tmpbody = strstr(value + tmppos, "=");
if (tmpbody != NULL)
if (strlen(tmpbody))
tmpbody++;
/* This is all a bit nested now... */
if (tmplen < 1 && tmpaddr == NULL) {
ci_write((FILE *) stream,
"500 No address found.\r\n");
goodtogo = 0;
} else if (tmpbody != NULL) {
/* See RFC 3030 for the best
* description of this stuff.
* */
if (strlen(tmpbody) < 4) {
/* Caught */
} else if (0 ==
strcasecmp(tmpbody,
"7BIT")) {
/* Sure fine go ahead. */
goodtogo = 1; // Not that it wasn't 1 already ;-)
}
/* 8BITMIME corresponds to RFC 1652,
* BINARYMIME corresponds to RFC 3030.
* */
else if (strlen(tmpbody) < 8) {
/* Caught */
} else if (0 ==
strcasecmp(tmpbody,
"8BITMIME"))
{
/* We can't do this yet. */
/* session->state = BIT8;
* */
ci_write((FILE *) stream,
"500 Please use 7BIT MIME only.\r\n");
goodtogo = 0;
} else if (strlen(tmpbody) < 10) {
/* Caught */
} else if (0 ==
strcasecmp(tmpbody,
"BINARYMIME"))
{
/* We can't do this yet. */
/* session->state = BDAT;
* */
ci_write((FILE *) stream,
"500 Please use 7BIT MIME only.\r\n");
goodtogo = 0;
}
}
if (goodtogo) {
/* Sure fine go ahead. */
dm_list_nodeadd(&from, tmpaddr, strlen(tmpaddr)+1);
ci_write((FILE *) stream,
"250 Sender <%s> OK\r\n",
(char *)(dm_list_getstart(&from)->data));
}
if (tmpaddr != NULL)
dm_free(tmpaddr);
}
return 1;
}
case LMTP_RCPT:
{
if (session->state != LHLO) {
ci_write((FILE *) stream,
"550 Command out of sequence.\r\n");
} else {
size_t tmplen = 0, tmppos = 0;
char *tmpaddr = NULL;
find_bounded(value, '<', '>', &tmpaddr,
&tmplen, &tmppos);
if (tmplen < 1) {
ci_write((FILE *) stream,
"500 No address found.\r\n");
} else {
/* Note that this is not a pointer, but really is on the stack!
* Because dm_list_nodeadd() memcpy's the structure, we don't need
* it to live any longer than the duration of this stack frame. */
deliver_to_user_t dsnuser;
dsnuser_init(&dsnuser);
/* find_bounded() allocated tmpaddr for us, and that's ok
* since dsnuser_free() will free it for us later on. */
dsnuser.address = tmpaddr;
if (dsnuser_resolve(&dsnuser) != 0) {
trace(TRACE_ERROR, "main(): dsnuser_resolve_list failed");
ci_write((FILE *) stream, "430 Temporary failure in recipient lookup\r\n");
dsnuser_free(&dsnuser);
return 1;
}
/* Class 2 means the address was deliverable in some way. */
switch (dsnuser.dsn.class) {
case DSN_CLASS_OK:
ci_write((FILE *) stream, "250 Recipient <%s> OK\r\n",
dsnuser.address);
/* A successfully found recipient goes onto the list.
* The struct will be free'd from lmtp_reset(). */
dm_list_nodeadd(&rcpt, &dsnuser, sizeof(deliver_to_user_t));
break;
default:
ci_write((FILE *) stream, "550 Recipient <%s> FAIL\r\n",
dsnuser.address);
/* If the user wasn't added, free the non-entry. */
dsnuser_free(&dsnuser);
break;
}
}
}
return 1;
}
/* Here's where it gets really exciting! */
case LMTP_DATA:
{
// if (session->state != DATA || session->state != BIT8)
if (session->state != LHLO) {
ci_write((FILE *) stream,
"550 Command out of sequence\r\n");
} else if (dm_list_length(&rcpt) < 1) {
ci_write((FILE *) stream,
"503 No valid recipients\r\n");
} else {
if (dm_list_length(&rcpt) > 0 && dm_list_length(&from) > 0) {
trace(TRACE_DEBUG,
"main(): requesting sender to begin message.");
ci_write((FILE *) stream,
"354 Start mail input; end with <CRLF>.<CRLF>\r\n");
} else {
if (dm_list_length(&rcpt) < 1) {
trace(TRACE_DEBUG,
"main(): no valid recipients found, cancel message.");
ci_write((FILE *) stream,
"503 No valid recipients\r\n");
}
if (dm_list_length(&from) < 1) {
trace(TRACE_DEBUG,
"main(): no sender provided, session cancelled.");
ci_write((FILE *) stream,
"554 No valid sender.\r\n");
}
return 1;
}
/* Anonymous Block */
{
struct element *element;
struct DbmailMessage *msg;
char *s;
if (! (msg = dbmail_message_new_from_stream((FILE *)instream, DBMAIL_STREAM_LMTP))) {
trace(TRACE_ERROR, "%s,%s: dbmail_message_new_from_stream() failed",
__FILE__, __func__);
discard_client_input((FILE *) instream);
ci_write((FILE *) stream, "500 Error reading message");
return 1;
}
s = dbmail_message_to_string(msg);
trace(TRACE_DEBUG, "%s,%s: whole message = %s", __FILE__, __func__, s);
g_free(s);
if (dbmail_message_get_hdrs_size(msg, FALSE) > READ_BLOCK_SIZE) {
trace(TRACE_ERROR, "%s,%s: header is too big",
__FILE__, __func__);
discard_client_input((FILE *) instream);
ci_write((FILE *)stream, "500 Error reading header, "
"header too big.\r\n");
return 1;
}
dbmail_message_set_header(msg, "Return-Path", from.start->data);
if (insert_messages(msg, &rcpt) == -1) {
ci_write((FILE *) stream, "503 Message not received\r\n");
} else {
/* The DATA command itself it not given a reply except
* that of the status of each of the remaining recipients. */
const char *class, *subject, *detail;
/* The replies MUST be in the order received */
rcpt.start =
dm_list_reverse(rcpt.start);
for (element = dm_list_getstart(&rcpt);
element != NULL;
element = element->nextnode) {
deliver_to_user_t * dsnuser =
(deliver_to_user_t *) element->data;
dsn_tostring(dsnuser->dsn, &class, &subject, &detail);
/* Give a simple OK, otherwise a detailed message. */
switch (dsnuser->dsn.class) {
case DSN_CLASS_OK:
ci_write((FILE *)stream, "%d%d%d Recipient <%s> OK\r\n",
dsnuser->dsn.class, dsnuser->dsn.subject, dsnuser->dsn.detail,
dsnuser->address);
break;
default:
ci_write((FILE *)stream, "%d%d%d Recipient <%s> %s %s %s\r\n",
dsnuser->dsn.class, dsnuser->dsn.subject, dsnuser->dsn.detail,
dsnuser->address, class, subject, detail);
}
}
}
dbmail_message_free(msg);
}
/* Reset the session after a successful delivery;
* MTA's like Exim prefer to immediately begin the
* next delivery without an RSET or a reconnect. */
lmtp_reset(session);
}
return 1;
}
default:
{
return lmtp_error(session, stream,
"500 What are you trying to say here?\r\n");
}
}
return 1;
}
syntax highlighted by Code2HTML, v. 0.9.1