/*
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 1700 2005-03-21 19:08:03Z aaron $
*
* implementation for lmtp commands according to RFC 1081 */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "dbmail.h"
#include "lmtp.h"
#include "pipe.h"
#include "header.h"
#include "db.h"
#include "dsn.h"
#include "debug.h"
#include "dbmailtypes.h"
#include "auth.h"
#include "clientinfo.h"
#include "lmtp.h"
#ifdef PROC_TITLES
#include "proctitleutils.h"
#endif
#define INCOMING_BUFFER_SIZE 512
#define MESSAGE_MAX_LINE_SIZE 1024
/* 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
/* These are needed across multiple calls to lmtp() */
static struct 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];
/**
* read the whole message from a network connection
* \param[in] instream input stream
* \param[out] whole_message will hold the complete email-message
* \param[out] whole_message_size will hold the size of the message
* \return
* - -1 on error
* - 1 on success
*/
static int read_whole_message_network(FILE *instream,
char **whole_message, u64_t *whole_message_size,
const char *prepend_format, ...) PRINTF_ARGS(4, 5);
/**
* \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 */
list_init(&rcpt);
list_init(&from);
}
int lmtp_reset(PopSession_t * session)
{
if (list_totalnodes(&rcpt) > 0) {
dsnuser_free_list(&rcpt);
}
list_init(&rcpt);
if (list_totalnodes(&from) > 0) {
list_freelist(&from.start);
}
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_ERROR,"%s,%s: database has gone away", __FILE__, __func__);
done=-1;
continue;
}
/* 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 (!ci->rx) {
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, &session);
}
fflush(ci->tx);
}
/* memory cleanup */
lmtp_reset(&session);
dm_free(buffer);
buffer = NULL;
/* reset timers */
alarm(0);
__debug_dumpallocs();
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 (list_totalnodes(&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 *)(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. */
list_nodeadd(&from, tmpaddr, strlen(tmpaddr)+1);
ci_write((FILE *) stream,
"250 Sender <%s> OK\r\n",
(char *)(list_getstart(&from)->data));
} else {
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 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(). */
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 (list_totalnodes(&rcpt) < 1) {
ci_write((FILE *) stream,
"503 No valid recipients\r\n");
} else {
if (list_totalnodes(&rcpt) > 0 && list_totalnodes(&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 (list_totalnodes(&rcpt) < 1) {
trace(TRACE_DEBUG,
"main(): no valid recipients found, cancel message.");
ci_write((FILE *) stream,
"503 No valid recipients\r\n");
}
if (list_totalnodes(&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 */
{
char *whole_message = NULL;
char *header = NULL;
const char *body;
u64_t whole_message_size;
u64_t headersize = 0;
u64_t headerrfcsize = 0;
u64_t body_size = 0;
u64_t body_rfcsize = 0;
u64_t dummyidx = 0, dummysize = 0;
struct list headerfields;
struct element *element;
if (read_whole_message_network(
(FILE *) instream,
&whole_message,
&whole_message_size,
"Return-Path: %s\r\n",
(char *)(list_getstart(&from)->data)) < 0) {
trace(TRACE_ERROR,
"%s,%s: read_whole_message_network() failed",
__FILE__, __func__);
discard_client_input((FILE *) instream);
ci_write((FILE *) stream,
"500 Error reading message");
return 1;
}
trace(TRACE_DEBUG, "%s,%s: whole message = %s",
__FILE__, __func__, whole_message);
if (whole_message == NULL) {
trace(TRACE_ERROR, "%s,%s message is NULL!",
__FILE__, __func__);
discard_client_input(
(FILE *) instream);
ci_write((FILE *) stream,
"500 Error reading header\r\n");
return 1;
}
if (split_message(whole_message,
whole_message_size - 1,
&header,
&headersize,
&headerrfcsize,
&body,
&body_size,
&body_rfcsize) < 0) {
trace(TRACE_ERROR, "%s,%s: split_message() failed",
__FILE__, __func__);
dm_free(whole_message);
discard_client_input((FILE *) instream);
ci_write((FILE *) stream,
"500 Error in message");
return 1;
}
if (headersize > READ_BLOCK_SIZE) {
trace(TRACE_ERROR,
"main(): header is too "
"big");
discard_client_input
((FILE *)
instream);
ci_write((FILE *)
stream,
"500 Error reading header, "
"header too big.\r\n");
dm_free(whole_message);
return 1;
}
/* Parse the list and scan for field and content.
* headerfields is filled here, and needs to be freed. */
list_init(&headerfields);
if (mime_readheader(header, &dummyidx,
&headerfields,
&dummysize) < 0) {
trace(TRACE_ERROR,
"main(): fatal error from mime_readheader()");
discard_client_input((FILE
*)
instream);
ci_write((FILE *) stream,
"500 Error reading header.\r\n");
dm_free(whole_message);
return 1;
}
if (insert_messages(
header, body,
headersize, headerrfcsize,
body_size, body_rfcsize,
&headerfields, &rcpt,
&from) == -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 =
dbmail_list_reverse(rcpt.start);
for (element = 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);
}
}
}
if (header != NULL)
dm_free(header);
if (whole_message != NULL)
dm_free(whole_message);
list_freelist(&headerfields.start);
}
/* 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;
}
int read_whole_message_network(FILE *instream,
char **whole_message, u64_t *whole_message_size,
const char *prepend_format, ...)
{
char *tmpmessage = NULL;
char tmpline[MESSAGE_MAX_LINE_SIZE + 1];
size_t line_size = 0;
size_t total_size = 0;
size_t current_pos = 0;
int error = 0;
va_list argp;
memset(tmpline, '\0', MESSAGE_MAX_LINE_SIZE + 1);
/* This adds the Return-Path header and any other
* important headers we might need; see RFC 2076. */
va_start(argp, prepend_format);
line_size = vsnprintf(tmpline, MESSAGE_MAX_LINE_SIZE, prepend_format, argp);
va_end(argp);
do {
line_size = strlen(tmpline);
/* It sometimes happens that we read a line of size 0,
which is odd.. For now, we just step over it. */
if (line_size < 2)
continue;
/* check for '.\r\n' */
if (line_size == 3 && strncmp(tmpline, ".\r\n", 3) == 0)
break;
/* change the \r\n ending to \n */
if (!(tmpmessage = dm_realloc(tmpmessage,
total_size + line_size - 1))) {
error = 1;
break;
}
if (!(memcpy((void *) &tmpmessage[current_pos],
(void *) tmpline, line_size -2))) {
error = 1;
break;
}
total_size += line_size - 1;
current_pos += line_size - 2;
tmpmessage[current_pos++] = '\n';
memset(tmpline, '\0', MESSAGE_MAX_LINE_SIZE + 1);
}
while (fgets(tmpline, MESSAGE_MAX_LINE_SIZE, instream) != NULL);
if (ferror(instream)) {
trace(TRACE_ERROR, "%s,%s: error reading instream",
__FILE__, __func__);
error = 1;
}
if (feof(instream)) {
trace(TRACE_ERROR, "%s,%s: unexpected EOF in instream",
__FILE__, __func__);
error = 1;
}
total_size += 1;
if (!(tmpmessage = dm_realloc(tmpmessage, total_size))) {
trace(TRACE_ERROR, "%s.%s: realloc failed",
__FILE__, __func__);
error = 1;
} else
tmpmessage[current_pos] = '\0';
if (error) {
trace(TRACE_ERROR, "%s,%s: error reading message from "
"instream", __FILE__, __func__);
dm_free(tmpmessage);
return -1;
}
*whole_message = tmpmessage;
*whole_message_size = total_size;
return 1;
}
syntax highlighted by Code2HTML, v. 0.9.1