view src/pop3_in.c @ 179:ec3fe72a3e99

Fixed an important bug with folded headers! g_strconcat() returns a *copy* of the string, but hdr->value still pointed to the old header (which probably was a memory leak, too). If the folded part had been quite small it was likely that the new string was at the same position as the old one, thus making everything go well. But if pretty long headers were folded several times it was likely that the new string was allocated somewhere else in memory, thus breaking things. In result mails to lots of recipients (folded header) were frequently only sent to the ones in the first line. Sorry for the inconvenience.
author meillo@marmaro.de
date Fri, 03 Jun 2011 09:52:17 +0200
parents 52c82d755215
children
line wrap: on
line source

/* pop3_in.c, Copyright (C) 2000 by Oliver Kurth,
 *
 * 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.
 */

/* see RFC 1725 */

#include <sys/wait.h>
#include <sys/stat.h>

#include "masqmail.h"
#include "pop3_in.h"
#include "readsock.h"

#ifdef USE_LIB_CRYPTO
#include <openssl/md5.h>
#else
#include "md5/md5.h"
#endif

#ifdef ENABLE_POP3

/* experimental feature */
#define DO_WRITE_UIDL_EARLY 1

static gchar*
MD5String(char *string)
{
	MD5_CTX context;
	unsigned char digest[16];
	char str_digest[33];
	int i;

#ifdef USE_LIB_CRYPTO
	MD5(string, strlen(string), digest);
#else
	MD5_Init(&context);
	MD5_Update(&context, string, strlen(string));
	MD5_Final(digest, &context);
#endif
	for (i = 0; i < 16; i++)
		sprintf(str_digest + 2 * i, "%02x", digest[i]);

	return g_strdup(str_digest);
}

static pop3_base*
create_pop3base(gint sock, guint flags)
{
	gint dup_sock;

	pop3_base *popb = (pop3_base *) g_malloc(sizeof(pop3_base));
	if (popb) {
		memset(popb, 0, sizeof(pop3_base));

		popb->error = pop3_ok;

		popb->buffer = (gchar *) g_malloc(POP3_BUF_LEN);

		dup_sock = dup(sock);
		popb->out = fdopen(sock, "w");
		popb->in = fdopen(dup_sock, "r");

		popb->flags = flags;
	}
	return popb;
}

static void
pop3_printf(FILE * out, gchar * fmt, ...)
{
	va_list args;
	va_start(args, fmt);

	DEBUG(4) {
		gchar buf[256];
		va_list args_copy;

		va_copy(args_copy, args);
		vsnprintf(buf, 255, fmt, args_copy);
		va_end(args_copy);

		debugf(">>>%s", buf);
	}

	vfprintf(out, fmt, args);
	fflush(out);

	va_end(args);
}

static gboolean
find_uid(pop3_base * popb, gchar * str)
{
	GList *node, *node_next;

	for (node = popb->list_uid_old; node; node = node_next) {
		gchar *uid = (gchar *) (node->data);
		node_next = node->next;
		if (strcmp(uid, str) == 0) {
#if 1
			popb->list_uid_old = g_list_remove_link(popb->list_uid_old, node);
			g_list_free_1(node);
			g_free(uid);
#endif
			return TRUE;
		}
	}
	return FALSE;
}

static gboolean
write_uidl(pop3_base * popb, gchar * user)
{
	gboolean ok = FALSE;
	GList *node;
	gchar *filename = g_strdup_printf("%s/popuidl/%s@%s", conf.spool_dir, user, popb->remote_host);
	gchar *tmpname = g_strdup_printf("%s.tmp", filename);
	FILE *fptr = fopen(tmpname, "wt");

	if (fptr) {
		foreach(popb->drop_list, node) {
			msg_info *info = (msg_info *) (node->data);
			if (info->is_fetched || info->is_in_uidl)
				fprintf(fptr, "%s\n", info->uid);
		}
		fclose(fptr);
		ok = (rename(tmpname, filename) != -1);
	}

	g_free(tmpname);
	g_free(filename);
	return ok;
}

static gboolean
read_uidl_fname(pop3_base * popb, gchar * filename)
{
	gboolean ok = FALSE;
	FILE *fptr = fopen(filename, "rt");
	gchar buf[256];

	if (fptr) {
		popb->list_uid_old = NULL;
		while (fgets(buf, 255, fptr)) {
			if (buf[strlen(buf) - 1] == '\n') {
				g_strchomp(buf);
				popb->list_uid_old = g_list_append(popb->list_uid_old, g_strdup(buf));
			} else {
				logwrite(LOG_ALERT, "broken uid: %s\n", buf);
				break;
			}
		}
		fclose(fptr);
		ok = TRUE;
	} else
		logwrite(LOG_ALERT, "opening of %s failed: %s", filename, strerror(errno));
	return ok;
}

static gboolean
read_uidl(pop3_base * popb, gchar * user)
{
	gboolean ok = FALSE;
	struct stat statbuf;
	gchar *filename = g_strdup_printf("%s/popuidl/%s@%s", conf.spool_dir, user, popb->remote_host);

	if (stat(filename, &statbuf) == 0) {
		ok = read_uidl_fname(popb, filename);
		if (ok) {
			GList *drop_node;
			foreach(popb->drop_list, drop_node) {
				msg_info *info = (msg_info *) (drop_node->data);
				if (find_uid(popb, info->uid)) {
					DEBUG(5) debugf("msg with uid '%s' already known\n", info->uid);
					info->is_in_uidl = TRUE;
					popb->uidl_known_cnt++;
				} else
					DEBUG(5) debugf("msg with uid '%s' not known\n", info->uid);
			}
		}
	} else {
		logwrite(LOG_DEBUG, "no uidl file '%s' found\n", filename);
		ok = TRUE;
	}

	g_free(filename);
	return ok;  /* return code is irrelevant, do not check... */
}

static gboolean
read_response(pop3_base * popb, int timeout)
{
	gint len;

	len = read_sockline(popb->in, popb->buffer, POP3_BUF_LEN, timeout, READSOCKL_CHUG);

	if (len == -3) {
		popb->error = pop3_timeout;
		return FALSE;
	} else if (len == -2) {
		popb->error = pop3_syntax;
		return FALSE;
	} else if (len == -1) {
		popb->error = pop3_eof;
		return FALSE;
	}

	return TRUE;
}

static gboolean
check_response(pop3_base * popb)
{
	char c = popb->buffer[0];

	if (c == '+') {
		popb->error = pop3_ok;
		return TRUE;
	} else if (c == '-')
		popb->error = pop3_fail;
	else
		popb->error = pop3_syntax;
	return FALSE;
}

static gboolean
strtoi(gchar * p, gchar ** pend, gint * val)
{
	gchar buf[12];
	gint i = 0;

	while (*p && isspace(*p))
		p++;
	if (*p) {
		while ((i < 11) && isdigit(*p))
			buf[i++] = *(p++);
		buf[i] = 0;
		*val = atoi(buf);
		*pend = p;
		return TRUE;
	}
	return FALSE;
}

static gboolean
check_response_int_int(pop3_base * popb, gint * arg0, gint * arg1)
{
	if (check_response(popb)) {
		gchar *p = &(popb->buffer[3]);
		gchar *pe;

		if (strtoi(p, &pe, arg0)) {
			DEBUG(5) debugf("arg0 = %d\n", *arg0);
			p = pe;
			if (strtoi(p, &pe, arg1))
				DEBUG(5) debugf("arg1 = %d\n", *arg1);
			return TRUE;
			/* FIXME: Paolo's code has the return stmt
			   inside the if block right above it. What
			   is correct? */
		}
		popb->error = pop3_syntax;
	}
	return FALSE;
}

static gboolean
get_drop_listing(pop3_base * popb)
{
	gchar buf[64];

	DEBUG(5) debugf("get_drop_listing() entered\n");

	while (1) {
		gint len = read_sockline(popb->in, buf, 64, POP3_CMD_TIMEOUT, READSOCKL_CHUG);
		if (len > 0) {
			if (buf[0] == '.')
				return TRUE;
			else {
				gint number, msg_size;
				gchar *p = buf, *pe;
				if (strtoi(p, &pe, &number)) {
					p = pe;
					if (strtoi(p, &pe, &msg_size)) {
						msg_info *info = g_malloc(sizeof(msg_info));
						info->number = number;
						info->size = msg_size;

						DEBUG(5) debugf ("get_drop_listing(), number = %d, msg_size = %d\n", number, msg_size);

						info->uid = NULL;
						info->is_fetched = FALSE;
						info->is_in_uidl = FALSE;
						popb->drop_list = g_list_append(popb->drop_list, info);
					} else {
						popb->error = pop3_syntax;
						break;
					}
				} else {
					popb->error = pop3_syntax;
					break;
				}
			}
		} else {
			popb->error = (len == -1) ? pop3_eof : pop3_timeout;
			return FALSE;
		}
	}
	return FALSE;
}

static gboolean
get_uid_listing(pop3_base * popb)
{
	gchar buf[64];

	while (1) {
		gint len = read_sockline(popb->in, buf, 64, POP3_CMD_TIMEOUT, READSOCKL_CHUG);
		if (len > 0) {
			if (buf[0] == '.')
				return TRUE;
			else {
				gint number;
				gchar *p = buf, *pe;
				if (strtoi(p, &pe, &number)) {
					msg_info *info = NULL;
					GList *drop_node;

					p = pe;
					while (*p && isspace(*p))
						p++;

					foreach(popb->drop_list, drop_node) {
						msg_info *curr_info = (msg_info *) (drop_node->data);
						if (curr_info->number == number) {
							info = curr_info;
							break;
						}
					}
					if (info) {
						info->uid = g_strdup(p);
						g_strchomp(info->uid);
					}

				} else {
					popb->error = pop3_syntax;
					break;
				}
			}
		}
	}
	return FALSE;
}

static gboolean
check_init_response(pop3_base * popb)
{
	if (check_response(popb)) {
		gchar buf[256];
		gchar *p = popb->buffer;
		gint i = 0;
		if (*p) {
			while (*p && (*p != '<'))
				p++;
			while (*p && (*p != '>') && (i < 254))
				buf[i++] = *(p++);
			buf[i++] = '>';
			buf[i] = '\0';

			popb->timestamp = g_strdup(buf);

			return TRUE;
		}
	}
	return FALSE;
}

void
pop3_in_close(pop3_base * popb)
{
	GList *node;

	fclose(popb->in);
	fclose(popb->out);

	close(popb->sock);

	foreach(popb->list_uid_old, node) {
		gchar *uid = (gchar *) (node->data);
		g_free(uid);
	}
	g_list_free(popb->list_uid_old);

	foreach(popb->drop_list, node) {
		msg_info *info = (msg_info *) (node->data);
		if (info->uid)
			g_free(info->uid);
		g_free(info);
	}
	g_list_free(popb->drop_list);

	if (popb->buffer)
		g_free(popb->buffer);
	if (popb->timestamp)
		g_free(popb->timestamp);
}

pop3_base*
pop3_in_open(gchar * host, gint port, GList * resolve_list, guint flags)
{
	pop3_base *popb;
	gint sock;
	mxip_addr *addr;

	DEBUG(5) debugf("pop3_in_open entered, host = %s\n", host);

	if ((addr = connect_resolvelist(&sock, host, port, resolve_list))) {
		/* create structure to hold status data: */
		popb = create_pop3base(sock, flags);
		popb->remote_host = addr->name;

		DEBUG(5) {
			struct sockaddr_in name;
			int len;
			getsockname(sock, (struct sockaddr *) (&name), &len);
			debugf("socket: name.sin_addr = %s\n", inet_ntoa(name.sin_addr));
		}
		return popb;
	}
	return NULL;
}

pop3_base*
pop3_in_open_child(gchar * cmd, guint flags)
{
	pop3_base *popb;
	gint sock;

	DEBUG(5) debugf("pop3_in_open_child entered, cmd = %s\n", cmd);
	sock = child(cmd);
	if (sock > 0) {
		popb = create_pop3base(sock, flags);
		popb->remote_host = NULL;
		return popb;
	}
	logwrite(LOG_ALERT, "child failed (sock = %d): %s\n", sock, strerror(errno));

	return NULL;
}

gboolean
pop3_in_init(pop3_base * popb)
{
	gboolean ok;

	if ((ok = read_response(popb, POP3_INITIAL_TIMEOUT))) {
		ok = check_init_response(popb);
	}
	if (!ok)
		/* pop3_in_log_failure(popb, NULL); */
		logwrite(LOG_ALERT, "pop3 failed\n");
	return ok;
}

gboolean
pop3_in_login(pop3_base * popb, gchar * user, gchar * pass)
{
	if (popb->flags & POP3_FLAG_APOP) {

		gchar *string = g_strdup_printf("%s%s", popb->timestamp, pass);
		gchar *digest = MD5String(string);
		pop3_printf(popb->out, "APOP %s %s\r\n", user, digest);
		g_free(string);
		g_free(digest);
		if (read_response(popb, POP3_CMD_TIMEOUT)) {
			if (check_response(popb))
				return TRUE;
			else
				popb->error = pop3_login_failure;
		}

	} else {

		pop3_printf(popb->out, "USER %s\r\n", user);
		if (read_response(popb, POP3_CMD_TIMEOUT)) {
			if (check_response(popb)) {
				pop3_printf(popb->out, "PASS %s\r\n", pass);
				if (read_response(popb, POP3_CMD_TIMEOUT)) {
					if (check_response(popb))
						return TRUE;
					else
						popb->error = pop3_login_failure;
				}
			} else {
				popb->error = pop3_login_failure;
			}
		}
	}
	return FALSE;
}

gboolean
pop3_in_stat(pop3_base * popb)
{
	pop3_printf(popb->out, "STAT\r\n");
	if (read_response(popb, POP3_CMD_TIMEOUT)) {
		gint msg_cnt, mbox_size;
		if (check_response_int_int(popb, &msg_cnt, &mbox_size)) {
			popb->msg_cnt = msg_cnt;
			popb->mbox_size = mbox_size;

			return TRUE;
		}
	}
	return FALSE;
}

gboolean
pop3_in_list(pop3_base * popb)
{
	pop3_printf(popb->out, "LIST\r\n");
	if (read_response(popb, POP3_CMD_TIMEOUT)) {
		if (get_drop_listing(popb)) {
			return TRUE;
		}
	}
	return FALSE;
}

gboolean
pop3_in_dele(pop3_base * popb, gint number)
{
	pop3_printf(popb->out, "DELE %d\r\n", number);
	if (read_response(popb, POP3_CMD_TIMEOUT)) {
		return TRUE;
	}
	return FALSE;
}

message*
pop3_in_retr(pop3_base * popb, gint number, address * rcpt)
{
	accept_error err;

	pop3_printf(popb->out, "RETR %d\r\n", number);
	if (read_response(popb, POP3_CMD_TIMEOUT)) {
		message *msg = create_message();
		msg->received_host = popb->remote_host;
		msg->received_prot = (popb->flags & POP3_FLAG_APOP) ? PROT_APOP : PROT_POP3;
		msg->transfer_id = (popb->next_id)++;
		msg->rcpt_list = g_list_append(NULL, copy_address(rcpt));

		if ((err = accept_message(popb->in, msg, ACC_MAIL_FROM_HEAD
		                          | (conf.do_save_envelope_to ? ACC_SAVE_ENVELOPE_TO : 0)))
		    == AERR_OK)
			return msg;

		destroy_message(msg);
	}
	return NULL;
}

gboolean
pop3_in_uidl(pop3_base * popb)
{
	pop3_printf(popb->out, "UIDL\r\n");
	if (read_response(popb, POP3_CMD_TIMEOUT)) {
		if (get_uid_listing(popb)) {
			return TRUE;
		}
	}
	return FALSE;
}

gboolean
pop3_in_quit(pop3_base * popb)
{
	pop3_printf(popb->out, "QUIT\r\n");
	DEBUG(4) debugf("QUIT\n");
	signal(SIGALRM, SIG_DFL);
	return TRUE;
}

/* Send a DELE command for each message in (the old) uid listing.
   This is to prevent mail from to be kept on server, if a previous
   transaction was interupted. */
gboolean
pop3_in_uidl_dele(pop3_base * popb)
{
	GList *drop_node;

	foreach(popb->drop_list, drop_node) {
		msg_info *info = (msg_info *) (drop_node->data);
		/* if(find_uid(popb, info->uid)){ */
		if (info->is_in_uidl) {
			if (!pop3_in_dele(popb, info->number))
				return FALSE;
			/* TODO: it probably makes sense to also delete this uid from the listing */
		}
	}
	return TRUE;
}

gboolean
pop3_get(pop3_base * popb, gchar * user, gchar * pass, address * rcpt, address * return_path,
         gint max_count, gint max_size, gboolean max_size_delete)
{
	gboolean ok = FALSE;
	gint num_children = 0;

	DEBUG(5) debugf("rcpt = %s@%s\n", rcpt->local_part, rcpt->domain);

	signal(SIGCHLD, SIG_DFL);

	if (pop3_in_init(popb)) {
		if (pop3_in_login(popb, user, pass)) {
			if (pop3_in_stat(popb)) {
				if (popb->msg_cnt > 0) {

					logwrite(LOG_NOTICE | LOG_VERBOSE, "%d message(s) for user %s at %s\n",
					         popb->msg_cnt, user, popb->remote_host);

					if (pop3_in_list(popb)) {
						gboolean do_get = !(popb->flags & POP3_FLAG_UIDL);
						if (!do_get)
							do_get = pop3_in_uidl(popb);
						if (do_get) {
							gint count = 0;
							GList *drop_node;

							if (popb->flags & POP3_FLAG_UIDL) {
								read_uidl(popb, user);
								logwrite(LOG_VERBOSE | LOG_NOTICE, "%d message(s) already in uidl.\n", popb->uidl_known_cnt);
							}
							if ((popb->flags & POP3_FLAG_UIDL) && (popb->flags & POP3_FLAG_UIDL_DELE))
								pop3_in_uidl_dele(popb);

							foreach(popb->drop_list, drop_node) {

								msg_info *info = (msg_info *) (drop_node->data);
								gboolean do_get_this = !(popb->flags & POP3_FLAG_UIDL);
								/* if(!do_get_this) do_get_this = !find_uid(popb, info->uid); */
								if (!do_get_this)
									do_get_this = !(info->is_in_uidl);
								if (do_get_this) {

									if ((info->size < max_size) || (max_size == 0)) {
										message *msg;

										logwrite(LOG_VERBOSE | LOG_NOTICE, "receiving message %d\n", info->number);
										msg = pop3_in_retr(popb, info->number, rcpt);

										if (msg) {
											if (return_path)
												msg->return_path = copy_address(return_path);
											if (spool_write(msg, TRUE)) {
												pid_t pid;
												logwrite(LOG_NOTICE, "%s <= %s host=%s with %s\n", msg->uid,
												         addr_string(msg->return_path), popb->remote_host,
												         (popb->flags & POP3_FLAG_APOP) ? prot_names [PROT_APOP] : prot_names [PROT_POP3]);
												info->is_fetched = TRUE;
												count++;
#if DO_WRITE_UIDL_EARLY
												if (popb->flags & POP3_FLAG_UIDL)
													write_uidl(popb, user);
#endif
												if (!conf.do_queue) {

													/* wait for child processes. If there are too many, we wait blocking, before we fork another one */
													while (num_children > 0) {
														int status, options = WNOHANG;
														pid_t pid;

														if (num_children >= POP3_MAX_CHILDREN) {
															logwrite(LOG_NOTICE, "too many children - waiting\n");
															options = 0;
														}
														if ((pid = waitpid(0, &status, options)) > 0) {
															num_children--;
															if (WEXITSTATUS(status) != EXIT_SUCCESS)
																logwrite(LOG_WARNING, "delivery process with pid %d returned %d\n", pid, WEXITSTATUS (status));
															if (WIFSIGNALED(status))
																logwrite(LOG_WARNING, "delivery process with pid %d got signal: %d\n", pid, WTERMSIG (status));
														} else if (pid < 0) {
															logwrite(LOG_WARNING, "wait got error: %s\n", strerror(errno));
														}
													}

													if ((pid = fork()) == 0) {
														deliver(msg);
														_exit(EXIT_SUCCESS);
													} else if (pid < 0) {
														logwrite(LOG_ALERT | LOG_VERBOSE, "could not fork for delivery, id = %s: %s\n", msg->uid, strerror(errno));
													} else
														num_children++;
												} else {
													DEBUG(1) debugf("queuing forced by configuration or option.\n");
												}
												if (popb->flags & POP3_FLAG_DELETE)
													pop3_in_dele(popb, info->number);

												destroy_message(msg);
											}	/* if(spool_write(msg, TRUE)) */
										} else {
											logwrite(LOG_ALERT, "retrieving of message %d failed: %d\n", info->number, popb->error);
										}
									} else {
										/* info->size > max_size */
										logwrite(LOG_NOTICE | LOG_VERBOSE, "size of message #%d (%d) > max_size (%d)\n", info->number, info->size, max_size);
										if (max_size_delete)
											if (popb->flags & POP3_FLAG_DELETE)
												pop3_in_dele(popb, info->number);
									}
								} /* if(do_get_this) ... */
								else {
									if (popb->flags & POP3_FLAG_UIDL) {
										info->is_fetched = TRUE;  /* obsolete? */
										logwrite(LOG_VERBOSE, "message %d already known\n", info->number);
										DEBUG(1) debugf("message %d (uid = %s) not fetched\n", info->number, info->uid);
#if 0
#if DO_WRITE_UIDL_EARLY
										write_uidl(popb, user);  /* obsolete? */
#endif
#endif
									}
								}
								if ((max_count != 0) && (count >= max_count))
									break;
							}	/* foreach() */
#if DO_WRITE_UIDL_EARLY
#else
							if (popb->flags & POP3_FLAG_UIDL)
								write_uidl(popb, user);
#endif
						}  /* if(pop3_in_uidl(popb) ... */
					}  /* if(pop3_in_list(popb)) */
				}  /* if(popb->msg_cnt > 0) */
				else {
					logwrite(LOG_NOTICE | LOG_VERBOSE, "no messages for user %s at %s\n", user, popb->remote_host);
				}
				ok = TRUE;
			}
			pop3_in_quit(popb);
		} else {
			logwrite(LOG_ALERT | LOG_VERBOSE, "pop3 login failed for user %s, host = %s\n", user, popb->remote_host);
		}
	}
	if (!ok) {
		logwrite(LOG_ALERT | LOG_VERBOSE, "pop3 failed, error = %d\n", popb->error);
	}

	while (num_children > 0) {
		int status;
		pid_t pid;
		if ((pid = wait(&status)) > 0) {
			num_children--;
			if (WEXITSTATUS(status) != EXIT_SUCCESS)
				logwrite(LOG_WARNING, "delivery process with pid %d returned %d\n", pid, WEXITSTATUS(status));
			if (WIFSIGNALED(status))
				logwrite(LOG_WARNING, "delivery process with pid %d got signal: %d\n", pid, WTERMSIG(status));
		} else {
			logwrite(LOG_WARNING, "wait got error: %s\n", strerror(errno));
		}
	}

	return ok;
}

/* function just to log into a pop server,
   for pop_before_smtp (or is it smtp_after_pop?)
*/

gboolean
pop3_login(gchar * host, gint port, GList * resolve_list, gchar * user, gchar * pass, guint flags)
{
	gboolean ok = FALSE;
	pop3_base *popb;

	signal(SIGCHLD, SIG_IGN);

	if ((popb = pop3_in_open(host, port, resolve_list, flags))) {
		if (pop3_in_init(popb)) {
			if (pop3_in_login(popb, user, pass))
				ok = TRUE;
			else
				logwrite(LOG_ALERT | LOG_VERBOSE, "pop3 login failed for user %s, host = %s\n", user, host);
		}
		pop3_in_close(popb);
	}
	return ok;
}

#endif