/* $Id: timsieve.c 1550 2005-01-07 12:23:02Z paul $
Copyright (C) 1999-2004 Aaron Stone aaron at serendipity dot cx
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.
*/
/* implementation for tims commands according to RFC 1081 */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include "dbmail.h"
#include "sort/sortsieve.h"
#include "timsieve.h"
#include "db.h"
#include "debug.h"
#include "dbmailtypes.h"
#include "auth.h"
#include "misc.h"
#include "clientinfo.h"
#ifdef PROC_TITLES
#include "proctitleutils.h"
#endif
#include <sieve2_interface.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
#define GREETING(stream) \
fprintf(stream, "\"IMPLEMENTATION\" \"DBMail timsieved v%s\"\r\n", VERSION); \
fprintf(stream, "\"SASL\" \"PLAIN\"\r\n"); \
fprintf(stream, "\"SIEVE\" \"%s\"\r\n", sieve2_listextensions()); \
fprintf(stream, "OK\r\n")
/* Remember, no trailing semicolon! */
/* Sadly, some client seem to be hardwired to look to 'timsieved'
* and so that part of the Implementation line is absolutely required. */
/* allowed timsieve commands */
static const char *commands[] = {
"LOGOUT", "STARTTLS", "CAPABILITY", "LISTSCRIPTS",
"AUTHENTICATE", "DELETESCRIPT", "GETSCRIPT", "SETACTIVE",
"HAVESPACE", "PUTSCRIPT"
};
/* \" is added to the standard set of stuff... */
static const char validchars[] =
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
"_.!@#$%^&*()-+=~[]{}<>:;\\\"`'/ ";
static char myhostname[64];
int tims_handle_connection(clientinfo_t * ci)
{
/*
Handles connection and calls
tims command handler
*/
int done = 1; /* loop state */
char *buffer = NULL; /* connection buffer */
int cnt; /* counter */
time_t timestamp;
PopSession_t session; /* current connection session */
/* setting Session variables */
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;
/* 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,
"tims_handle_connection(): Could not allocate buffer");
return 0;
}
if (ci->tx) {
/* This is a macro shared with TIMS_CAPA, per the draft RFC. */
GREETING(ci->tx);
fflush(ci->tx);
} else {
trace(TRACE_MESSAGE,
"tims_handle_connection(): TX stream is null!");
dm_free(buffer);
return 0;
}
while (done > 0) {
/* 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)) {
if (cnt > 0) {
/* Ignore single newlines and \r\n pairs */
if (cnt != 1
|| buffer[cnt - 1] != '\r') {
buffer[cnt + 1] = '\0';
break;
}
/* Overwrite those silly extra \r\n's */
else {
/* Incremented to 0 at top of loop */
cnt = -1;
}
}
}
}
if (feof(ci->rx) || ferror(ci->rx)) {
/* check client eof */
done = -1;
} else {
/* reset function handle timeout */
alarm(0);
/* handle tims commands */
done =
tims(ci->tx, ci->rx, buffer, ci->ip, &session);
}
fflush(ci->tx);
}
/* memory cleanup */
dm_free(buffer);
buffer = NULL;
/* reset timers */
alarm(0);
__debug_dumpallocs();
return 0;
}
int tims_reset(PopSession_t * session)
{
session->state = STRT;
return 1;
}
int tims_error(PopSession_t * session, void *stream,
const char *formatstring, ...)
{
va_list argp;
if (session->error_count >= MAX_ERRORS) {
trace(TRACE_MESSAGE,
"tims_error(): too many errors (MAX_ERRORS is %d)",
MAX_ERRORS);
fprintf((FILE *) stream,
"BYE \"Too many errors, closing connection.\"\r\n");
session->SessionResult = 2; /* possible flood */
tims_reset(session);
return -3;
} else {
va_start(argp, formatstring);
vfprintf((FILE *) stream, formatstring, argp);
va_end(argp);
}
trace(TRACE_DEBUG, "tims_error(): an invalid command was issued");
session->error_count++;
return 1;
}
int tims(void *stream, void *instream, char *buffer, char *client_ip,
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, "tims(): buffer overflow attempt");
return -3;
}
/* check for command issued */
while (strchr(validchars, buffer[indx]))
indx++;
/* end buffer */
buffer[indx] = '\0';
trace(TRACE_DEBUG, "tims(): 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,
"tims(): command issued: cmd [%s], val [%s]",
command, value);
}
}
for (cmdtype = TIMS_STRT; cmdtype < TIMS_END; cmdtype++)
if (strcasecmp(command, commands[cmdtype]) == 0)
break;
trace(TRACE_DEBUG, "tims(): command looked up as commandtype %d",
cmdtype);
/* commands that are allowed to have no arguments */
if ((value == NULL) && !(cmdtype < TIMS_NOARGS)
&& (cmdtype < TIMS_END)) {
return tims_error(session, stream,
"NO \"This command requires an argument.\"\r\n");
}
switch (cmdtype) {
case TIMS_LOUT:
{
fprintf((FILE *) stream, "OK\r\n");
tims_reset(session);
return 0; /* return 0 to cause the connection to close */
}
case TIMS_STLS:
{
/* We don't support TLS, sorry! */
fprintf((FILE *) stream, "NO\r\n");
return 1;
}
case TIMS_CAPA:
{
/* This is macro-ized because it is also used in the greeting. */
GREETING((FILE *) stream);
return 1;
}
case TIMS_AUTH:
{
/* We currently only support plain authentication,
* which means that the command we accept will look
* like this: Authenticate "PLAIN" "base64-password"
* */
if (strlen(value) > strlen("\"PLAIN\"")) {
/* Only compare the first part of value */
if (strncasecmp
(value, "\"PLAIN\"",
strlen("\"PLAIN\"")) == 0) {
size_t tmplen = 0;
size_t tmppos = 0;
char *tmpleft = NULL, **tmp64 =
NULL;
/* First see if the base64 SASL is simply quoted */
if (0 !=
find_bounded(value +
strlen
("\"PLAIN\""),
'"', '"',
&tmpleft, &tmplen,
&tmppos)) {
u64_t authlen; /* Actually, script length must be 32-bit unsigned int. */
char tmpcharlen[11]; /* A 32-bit unsigned int is ten decimal digits in length. */
/* Second, failing that, see if it's an {n+} literal */
find_bounded(value +
strlen
("\"PLAIN\""),
'{', '+',
&tmpleft,
&tmplen,
&tmppos);
strncpy(tmpcharlen,
tmpleft,
(10 <
tmplen ? 10 :
tmplen));
tmpcharlen[(10 <
tmplen ? 10 :
tmplen)] =
'\0';
dm_free(tmpleft);
authlen =
strtoull(tmpcharlen,
NULL, 10);
if (authlen >= UINT_MAX) {
fprintf((FILE *)
stream,
"NO \"Invalid SASL length.\"\r\n");
tmplen = 0; /* HACK: This prevents the next block from running. */
} else {
if (0 !=
read_from_stream
((FILE *)
instream,
&tmpleft,
authlen)) {
fprintf((FILE *) stream, "NO \"Error reading SASL.\"\r\n");
} else {
tmplen = authlen; /* HACK: This allows the next block to run. */
}
}
}
if (tmplen < 1) {
/* Definitely an empty password string */
fprintf((FILE *) stream,
"NO \"Password required.\"\r\n");
} else {
size_t i;
u64_t useridnr;
tmp64 =
base64_decode(tmpleft,
tmplen);
if (tmp64 == NULL) {
fprintf((FILE *)
stream,
"NO \"SASL decode error.\"\r\n");
} else {
for (i = 0; tmp64[i] != NULL; i++) { /* Just count 'em up */
}
if (i < 3) {
fprintf((FILE *) stream, "NO \"Too few encoded SASL arguments.\"\r\n");
}
/* The protocol specifies that the base64 encoding
* be made up of three parts: proxy, username, password
* Between them are NULLs, which are conveniently encoded
* by the base64 process... */
if (auth_validate
(tmp64[1],
tmp64[2],
&useridnr) ==
1) {
fprintf((FILE *) stream, "OK\r\n");
session->
state =
AUTH;
session->
useridnr
=
useridnr;
session->
username
=
dm_strdup
(tmp64
[1]);
session->
password
=
dm_strdup
(tmp64
[2]);
} else {
fprintf((FILE *) stream, "NO \"Username or password incorrect.\"\r\n");
}
for (i = 0;
tmp64[i] !=
NULL; i++) {
dm_free
(tmp64
[i]);
}
dm_free(tmp64);
}
} /* if... tmplen < 1 */
} /* if... strncasecmp() == "PLAIN" */
else {
trace(TRACE_INFO,
"tims(): Input simply was not PLAIN auth");
fprintf((FILE *) stream,
"NO \"Authentication scheme not supported.\"\r\n");
}
} /* if... strlen() < "PLAIN" */
else {
trace(TRACE_INFO,
"tims(): Input too short to possibly be PLAIN auth");
fprintf((FILE *) stream,
"NO \"Authentication scheme not supported.\"\r\n");
}
return 1;
}
case TIMS_PUTS:
{
if (session->state != AUTH) {
fprintf((FILE *) stream,
"NO \"Please authenticate first.\"\r\n");
} else {
size_t tmplen = 0;
size_t tmppos = 0;
char *tmpleft = NULL;
find_bounded(value, '"', '"', &tmpleft,
&tmplen, &tmppos);
if (tmplen < 1) {
/* Possibly an empty password... */
fprintf((FILE *) stream,
"NO \"Script name required.\"\r\n");
} else {
char scriptname
[MAX_SIEVE_SCRIPTNAME + 1];
strncpy(scriptname, tmpleft,
(MAX_SIEVE_SCRIPTNAME <
tmplen ?
MAX_SIEVE_SCRIPTNAME :
tmplen));
/* Of course, be sure to NULL terminate, because strncpy() likely won't */
scriptname[(MAX_SIEVE_SCRIPTNAME <
tmplen ?
MAX_SIEVE_SCRIPTNAME :
tmplen)] = '\0';
dm_free(tmpleft);
/* Offset from the previous match to make sure not to pull
* the "length" from a script with a malicious name */
find_bounded(value + tmppos, '{',
'+', &tmpleft,
&tmplen, &tmppos);
if (tmplen < 1) {
/* Possibly an empty password... */
fprintf((FILE *) stream,
"NO \"Length required.\"\r\n");
} else {
u64_t scriptlen; /* Actually, script length must be 32-bit unsigned int. */
char tmpcharlen[11]; /* A 32-bit unsigned int is ten decimal digits in length. */
strncpy(tmpcharlen,
tmpleft,
(10 <
tmplen ? 10 :
tmplen));
tmpcharlen[(10 <
tmplen ? 10 :
tmplen)] =
'\0';
dm_free(tmpleft);
scriptlen =
strtoull(tmpcharlen,
NULL, 10);
trace(TRACE_INFO,
"%s, %s: Client sending script of length [%llu]",
__FILE__,
__func__,
scriptlen);
if (scriptlen >= UINT_MAX) {
trace(TRACE_INFO,
"%s, %s: Length [%llu] is larger than UINT_MAX [%u]",
__FILE__,
__func__,
scriptlen,
UINT_MAX);
fprintf((FILE *)
stream,
"NO \"Invalid script length.\"\r\n");
} else {
char *f_buf = NULL;
if (0 !=
read_from_stream
((FILE *)
instream,
&f_buf,
scriptlen)) {
trace
(TRACE_INFO,
"%s, %s: Error reading script with read_from_stream()",
__FILE__,
__func__);
fprintf((FILE *) stream, "NO \"Error reading script.\"\r\n");
} else {
if (0 !=
db_check_sievescript_quota
(session->
useridnr,
scriptlen))
{
trace
(TRACE_INFO,
"%s, %s: Script exceeds user's quota, dumping it",
__FILE__,
__func__);
fprintf
((FILE *) stream, "NO \"Script exceeds available space.\"\r\n");
} else {
char *errmsg = NULL;
if (0 != sortsieve_script_validate(f_buf, &errmsg)) {
trace
(TRACE_INFO,
"%s, %s: Script has syntax errrors: [%s]",
__FILE__,
__func__,
errmsg);
fprintf
((FILE *) stream, "NO \"Script error: %s.\"\r\n", errmsg);
} else {
/* According to the draft RFC, a script with the same
* name as an existing script should [atomically] replace it. */
if (0 != db_replace_sievescript(session->useridnr, scriptname, f_buf)) {
trace
(TRACE_INFO,
"%s, %s: Error inserting script",
__FILE__,
__func__);
fprintf
((FILE *) stream, "NO \"Error inserting script.\"\r\n");
} else {
trace
(TRACE_INFO,
"%s, %s: Script successfully received",
__FILE__,
__func__);
fprintf
((FILE *) stream, "OK \"Script successfully received.\"\r\n");
}
}
dm_free
(f_buf);
}
}
}
}
}
}
return 1;
}
case TIMS_SETS:
{
if (session->state != AUTH) {
fprintf((FILE *) stream,
"NO \"Please authenticate first.\"\r\n");
} else {
int ret;
size_t tmplen = 0;
size_t tmppos = 0;
char *tmpleft = NULL;
find_bounded(value, '"', '"', &tmpleft,
&tmplen, &tmppos);
/* Only activate a new script if one was specified */
if (tmplen > 0) {
char scriptname
[MAX_SIEVE_SCRIPTNAME + 1];
strncpy(scriptname, tmpleft,
(MAX_SIEVE_SCRIPTNAME <
tmplen ?
MAX_SIEVE_SCRIPTNAME :
tmplen));
/* Of course, be sure to NULL terminate, because strncpy() likely won't */
scriptname[(MAX_SIEVE_SCRIPTNAME <
tmplen ?
MAX_SIEVE_SCRIPTNAME :
tmplen)] = '\0';
dm_free(tmpleft);
ret =
db_activate_sievescript
(session->useridnr,
scriptname);
if (ret == -3) {
fprintf((FILE *) stream,
"NO \"Script does not exist.\"\r\n");
return -1;
} else if (ret != 0) {
fprintf((FILE *) stream,
"NO \"Internal error.\"\r\n");
return -1;
} else {
fprintf((FILE *) stream,
"OK \"Script activated.\"\r\n");
}
} else {
char *scriptname = NULL;
ret =
db_get_sievescript_active
(session->useridnr,
&scriptname);
if (scriptname == NULL) {
fprintf((FILE *) stream,
"OK \"No scripts are active at this time.\"\r\n");
} else {
ret =
db_deactivate_sievescript
(session->useridnr,
scriptname);
dm_free(scriptname);
if (ret == -3) {
fprintf((FILE *)
stream,
"NO \"Active script does not exist.\"\r\n");
return -1;
} else if (ret != 0) {
fprintf((FILE *)
stream,
"NO \"Internal error.\"\r\n");
return -1;
} else {
fprintf((FILE *)
stream,
"OK \"All scripts deactivated.\"\r\n");
}
}
}
}
return 1;
}
case TIMS_GETS:
{
if (session->state != AUTH) {
fprintf((FILE *) stream,
"NO \"Please authenticate first.\"\r\n");
} else {
size_t tmplen = 0;
size_t tmppos = 0;
char *tmpleft = NULL;
find_bounded(value, '"', '"', &tmpleft,
&tmplen, &tmppos);
if (tmplen < 1) {
/* Possibly an empty password... */
fprintf((FILE *) stream,
"NO \"Script name required.\"\r\n");
} else {
int ret = 0;
char *script = NULL;
char scriptname
[MAX_SIEVE_SCRIPTNAME + 1];
strncpy(scriptname, tmpleft,
(MAX_SIEVE_SCRIPTNAME <
tmplen ?
MAX_SIEVE_SCRIPTNAME :
tmplen));
/* Of course, be sure to NULL terminate, because strncpy() likely won't */
scriptname[(MAX_SIEVE_SCRIPTNAME <
tmplen ?
MAX_SIEVE_SCRIPTNAME :
tmplen)] = '\0';
dm_free(tmpleft);
ret =
db_get_sievescript_byname
(session->useridnr, scriptname,
&script);
if (ret == -3) {
fprintf((FILE *) stream,
"NO \"Script not found.\"\r\n");
} else if (ret != 0
|| script == NULL) {
fprintf((FILE *) stream,
"NO \"Internal error.\"\r\n");
} else {
fprintf((FILE *) stream,
"{%u+}\r\n",
strlen(script));
fprintf((FILE *) stream,
"%s\r\n", script);
fprintf((FILE *) stream,
"OK\r\n");
}
}
}
return 1;
}
case TIMS_DELS:
{
if (session->state != AUTH) {
fprintf((FILE *) stream,
"NO \"Please authenticate first.\"\r\n");
} else {
size_t tmplen = 0;
size_t tmppos = 0;
char *tmpleft = NULL;
find_bounded(value, '"', '"', &tmpleft,
&tmplen, &tmppos);
if (tmplen < 1) {
/* Possibly an empty password... */
fprintf((FILE *) stream,
"NO \"Script name required.\"\r\n");
} else {
int ret = 0;
char scriptname
[MAX_SIEVE_SCRIPTNAME + 1];
strncpy(scriptname, tmpleft,
(MAX_SIEVE_SCRIPTNAME <
tmplen ?
MAX_SIEVE_SCRIPTNAME :
tmplen));
/* Of course, be sure to NULL terminate, because strncpy() likely won't */
scriptname[(MAX_SIEVE_SCRIPTNAME <
tmplen ?
MAX_SIEVE_SCRIPTNAME :
tmplen)] = '\0';
dm_free(tmpleft);
ret =
db_delete_sievescript(session->
useridnr,
scriptname);
if (ret == -3) {
fprintf((FILE *) stream,
"NO \"Script not found.\"\r\n");
} else if (ret != 0) {
fprintf((FILE *) stream,
"NO \"Internal error.\"\r\n");
} else {
fprintf((FILE *) stream,
"OK\r\n");
}
}
}
return 1;
}
case TIMS_SPAC:
{
if (session->state != AUTH) {
fprintf((FILE *) stream,
"NO \"Please authenticate first.\"\r\n");
} else {
fprintf((FILE *) stream,
"NO \"Command not implemented.\"\r\n");
}
return 1;
}
case TIMS_LIST:
{
if (session->state != AUTH) {
fprintf((FILE *) stream,
"NO \"Please authenticate first.\"\r\n");
} else {
struct list scriptlist;
struct element *tmp;
if (db_get_sievescript_listall
(session->useridnr, &scriptlist) < 0) {
fprintf((FILE *) stream,
"NO \"Internal error.\"\r\n");
} else {
if (list_totalnodes(&scriptlist) ==
0) {
/* The command hasn't failed, but there aren't any scripts */
fprintf((FILE *) stream,
"OK \"No scripts found.\"\r\n");
} else {
tmp =
list_getstart
(&scriptlist);
while (tmp != NULL) {
struct ssinfo *info
=
(struct ssinfo
*) tmp->data;
fprintf((FILE *)
stream,
"\"%s\"%s\r\n",
info->name,
(info->
active ==
1 ?
" ACTIVE"
: ""));
tmp =
tmp->nextnode;
}
fprintf((FILE *) stream,
"OK\r\n");
}
if (scriptlist.start)
list_freelist(&scriptlist.
start);
}
}
return 1;
}
default:
{
return tims_error(session, stream,
"NO \"What are you trying to say here?\"\r\n");
}
}
return 1;
}
syntax highlighted by Code2HTML, v. 0.9.1