/* $Id: rules.cpp,v 1.8 2004/01/02 03:54:01 fesnel Exp $ */
/*******************************************************************************
 *   This program is part of a library used by the Archimedes email client     * 
 *                                                                             *
 *   Copyright : (C) 1995-1998 Gennady B. Sorokopud (gena@NetVision.net.il)    *
 *               (C) 1995 Ugen. J. S. Antsilevich (ugen@latte.worldbank.org)   *
 *               (C) 1998-2004 by the Archimedes Project                       *
 *                   http://sourceforge.net/projects/archimedes                *
 *                                                                             *
 *             --------------------------------------------                    *
 *                                                                             *
 *   This program is free software; you can redistribute it and/or modify      *
 *   it under the terms of the GNU Library 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 Library General Public License for more details.                      *
 *                                                                             *
 *   You should have received a copy of the GNU Library General Public License *
 *   along with this program; if not, write to the Free Software               *
 *   Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307, USA.  *
 *                                                                             *
 ******************************************************************************/


#include <fmail.h>
#include <umail.h>
//#include <choose_folder.h>

#include "addrbook_util.h"

/* Prototypes */
static struct _xf_rule *match_msg(struct _mail_msg *msg, u_int type);
static int apply_rule_opts(struct _xf_rule *rule, struct _mail_msg *msg);
static u_int match_news_addr(struct _news_addr * addr, struct _xf_rule * rule);
static u_int match_addr(mail_addr * addr, struct _xf_rule * rule);

extern cfgfile Config;

/* Rules */
vector<struct _xf_rule *> rules;

void rule_rename_folder(struct _mail_folder *newfld, char *oldname)
{
	int i;

	if(!oldname || !newfld)
		return;

	for(i = 0; i < (int)rules.size(); i++)
		if(rules[i]->action == R_MOVE)
			if(!strcmp(rules[i]->data, oldname))
				snprintf(rules[i]->data, sizeof(rules[i]->data), "%s",
					get_folder_full_name(newfld));

	save_rules();
}

int rule_by_name(char *rname) {
	int i;

	for(i = 0; i < (int)rules.size(); i++) {
		if((rules[i]->action != 0) && !strcasecmp(rname, rules[i]->name))
			return i;
	}

	return -1;
}

int load_rules() {
	FILE *rfd = NULL;
	char buf[255];
	char rfname[MAXPATHLEN];
	struct _xf_rule * new_rule = NULL;

#ifdef HAVE_REGCOMP
	char errbuf[2048];
	int regerr = 0;
	int regflags = 0;
#endif

	snprintf(rfname, sizeof(rfname), "%s/.xfmrules", configdir);

	if((rfd = fopen(rfname, "r+")) == NULL) {
		display_msg(MSG_WARN, "Can not read rules database", "%s", rfname);
		save_rules();	/* create an empty .xfmrules database */
		return -1;
	}

	/* Clear current rules databases */
	cleanup_rules();

	fseek(rfd, 0L, SEEK_SET);
	while(fgets(buf, sizeof(buf), rfd)) {
		if(buf[0] != '@')
			continue;

		strip_newline(buf);

		new_rule = (struct _xf_rule *) malloc(sizeof(struct _xf_rule));
		init_rule(new_rule);

		/* XXX: could overflow */
		sscanf(buf + 1, "%s %d %d %s %s", new_rule->name, &new_rule->action,
			&new_rule->flags, new_rule->data, new_rule->fmatch);

		if(fgets(buf, sizeof(buf), rfd)) {
			if(strlen(buf))
				strip_newline(buf);
			snprintf(new_rule->tmatch, sizeof(new_rule->tmatch), "%s", buf);
		} else
			new_rule->tmatch[0] = '\0';
	
#ifdef HAVE_REGCOMP
		regflags = REG_EXTENDED;
		if(new_rule->flags & CASE_I)
			regflags |= REG_ICASE;

		if((regerr = regcomp(&new_rule->rx, new_rule->tmatch, regflags))) {
			regerror(regerr, &new_rule->rx, errbuf, sizeof(errbuf));
			display_msg(MSG_WARN, "Invalid regular expression", "%s", errbuf);
			regfree(&new_rule->rx);
			free(new_rule);
			continue;
		}
#endif

		if((new_rule->action < 1) || (new_rule->action > RULE_MAXACTION)) {
			display_msg(MSG_WARN, "rules", "Invalid action code %d",
				new_rule->action);
			free(new_rule);
			continue;
		}

		rules.push_back(new_rule);
	}	
	fclose(rfd);
	return 0;
}

int cleanup_rules() {
	int i;
	
	for(i = 0; i < (int)rules.size(); i++) {
#ifdef HAVE_REGCOMP
		regfree(&rules[i]->rx);
#endif
		free(rules[i]);
	}
	
	rules.clear();
	return 0;
}

int save_rules() {
	FILE *rfd;
	char rfname[MAXPATHLEN];
	int i;

	if(readonly)
		return 0;

	snprintf(rfname, sizeof(rfname), "%s/.xfmrules", configdir);

	if(!(rfd = fopen(rfname, "w"))) {
		display_msg(MSG_WARN, "Can not save rules database", "%s", rfname);
		return -1;
	}

	for(i = 0; i < (int)rules.size(); i++) {
		//if(rules[i]->action == 0)
			//continue;
		
		fprintf(rfd, "@%s %d %d %s %s\n", rules[i]->name, rules[i]->action,
				rules[i]->flags, rules[i]->data, rules[i]->fmatch);
		fprintf(rfd, "%s\n", rules[i]->tmatch);
		
	}

	fclose(rfd);
	return 0;
}

/* Check if addr matches given rule,
 * returns 1 if match is successful and 0 if not */
static u_int match_news_addr(struct _news_addr * addr, struct _xf_rule * rule) {
	if(!addr || !rule)
		return 0;

	while(addr) {
#ifdef  HAVE_REGCOMP
		if(regexec(&rule->rx, addr->name, 0, 0, 0) == 0)
			return 1;

		if(addr->descr && (regexec(&rule->rx, addr->descr, 0, 0, 0) == 0))
			return 1;
#else
		if(strcasestr(addr->name, rules->tmatch, rules->flags & CASE_I))
			return 1;

		if(addr->descr &&
		   strcasestr(addr->descr, rules->tmatch, rules->flags & CASE_I))
			return 1;
#endif
		addr = addr->next;
	}

	return 0;
}

/* check if addr matches given rule */
/* returns 1 if matches and 0 if not */
static u_int match_addr(mail_addr * addr, struct _xf_rule * rule) {
	if(!addr || !rule)
		return 0;

	while(addr) {
#ifdef  HAVE_REGCOMP
		if(regexec(&rule->rx, addr->addr, 0, 0, 0) == 0)
			return 1;

		if(addr->name && (regexec(&rule->rx, addr->name, 0, 0, 0) == 0))
			return 1;

		if(addr->comment
		   && (regexec(&rule->rx, addr->comment, 0, 0, 0) == 0))
			return 1;
#else
		if(strcasestr(addr->addr, rules->tmatch, rules->flags & CASE_I))
			return 1;

		if(addr->name &&
		   strcasestr(addr->name, rules->tmatch, rules->flags & CASE_I))
			return 1;

		if(addr->comment &&
		   strcasestr(addr->comment, rules->tmatch,
					  rules->flags & CASE_I))
			return 1;
#endif

		addr = addr->next_addr;
	}

	return 0;
}

void init_rule(struct _xf_rule *rule) {
	if(!rule)
		return;

	*rule->name = '\0';
	*rule->fmatch = '\0';
	*rule->tmatch = '\0';
	*rule->data = '\0';

	rule->action = 0;
	rule->flags = 0;
}

int match_rule(struct _mail_msg *msg, struct _xf_rule *rule) {
	struct _mail_addr *addr;
	int fr;
	char *p, c;

	if(!msg || !rule) {
		return 0;
	}

	if((strcasecmp(rule->fmatch, "Header") != 0)
	   && (addr = get_addr_by_name(msg, rule->fmatch)) != NULL) {
		return match_addr(addr, rule);
	}

	if(!strcasecmp(rule->fmatch, "Newsgroups")) {
		return match_news_addr(msg->header->News, rule);
	}

	if(!strcasecmp(rule->fmatch, "Recipients")) {
		msg->get_header(msg);
		if(match_addr(msg->header->To, rule))
			return 1;
		if(match_addr(msg->header->Cc, rule))
			return 1;
		if(match_addr(msg->header->Bcc, rule))
			return 1;
		if(match_news_addr(msg->header->News, rule))
			return 1;

		return 0;
	}

	if(!(p = get_field_content(msg, rule->fmatch, &fr))) {
		return 0;
	}

	/* XXX: There's a flaw in the design of the mmaping code that
	 * if the length of the message body is a multiple of the page
	 * length, then the message body might not be
	 * null-terminated. This is because mmap won't mmap more than
	 * the file length. Therefore, the body field has to be
	 * truncated so that the regexec/strcasestr doesn't run off
	 * the end of the string. The last character is restored after
	 * the search is done.
	 *
	 * We hope that removing the last character from the mail
	 * doesn't affect what the user was searching for.
	 */
	c = -1;
	if (strncasecmp(rule->fmatch, "Body", 4) == 0 && fr == 1) {
	    c = msg->msg_body[msg->msg_body_len - 1];
	    msg->msg_body[msg->msg_body_len - 1] = '\0';
	}

#ifdef HAVE_REGCOMP
	if(regexec(&rule->rx, p, 0, 0, 0) == 0) {
		if (c != -1)
			msg->msg_body[msg->msg_body_len - 1] = c;
		free_field_content(msg, p, fr);
		return 1;
	}
#else
	if(strcasestr(p, rule->tmatch, rule->flags & CASE_I)) {
		if (c != -1)
			msg->msg_body[msg->msg_body_len - 1] = c;
		free_field_content(msg, p, fr);
		return 1;
	}
#endif

	if (c != -1)
		msg->msg_body[msg->msg_body_len - 1] = c;
	free_field_content(msg, p, fr);

	return 0;
}

/* check if msg matches any rule. */
/* if yes, returns pointer to this rule, otherwise returns NULL */
/* it type != 0 only rules of this type are checked */
static struct _xf_rule *match_msg(struct _mail_msg *msg, u_int type) {
	int i;

	if(!msg)
		return NULL;

	for(i = 0; i < (int)rules.size(); i++) {
		//if(rules[i]->action == 0) {
			//continue;
		//}

		if(type != 0 && rules[i]->action != type) {
			continue;
		}

		if((msg->status & NOTINCOMING) && !(rules[i]->flags & R_NINCOMING)) {
			continue;
		}

		if(!(msg->status & NOTINCOMING) && (rules[i]->flags & R_NINCOMING)) {
			continue;
		}

		if((msg->status & MOUTGOING) && !(rules[i]->flags & R_OUTGOING)) {
			continue;
		}

		if(!(msg->status & MOUTGOING) && (rules[i]->flags & R_OUTGOING)) {
			continue;
		}

		if(match_rule(msg, &(*rules[i]))) {
			if((rules[i]->flags & R_NINCOMING) && (msg->status & RECENT))
				continue;
			if(rules[i]->flags & R_LOG) {
				display_msg(MSG_LOG, "rule",
							"%s matched message %ld in %s folder",
							rules[i]->name, msg->uid,
							msg->folder ? msg->folder->sname : "incoming");
			}
			msg->free_text(msg);
			return &(*rules[i]);
		}
	}

	msg->free_text(msg);
	return NULL;
}

static int apply_rule_opts(struct _xf_rule *rule, struct _mail_msg *msg) {
	if(!(msg->status & RECENT))
		return 0;

	if(rule->flags & R_SAVE_ADDR)
		add_msg_addr(msg, "default");

	if(rule->flags & R_MARK_READ)
		msg->flags &= ~UNREAD;

	return(rule->flags & R_SILENT) ? 1 : 0;
}

int apply_rule(struct _mail_msg *msg, int delay) {
	struct _mail_folder *folder;
	struct _mail_msg *msg1;
	struct _mail_addr *addr;
	struct _head_field *hf;
	struct _xf_rule *rule;
	struct _retrieve_src *source;
	int action, mcount = 0, eres, mnum = 0;
	char *data = INBOX, buf[MAXPATHLEN], mfile[MAXPATHLEN], *p;
	struct _proc_info pinfo;
	struct stat sb;

	if((rule = match_msg(msg, 0)) == NULL) {
		if(msg->status & NOTINCOMING)
			return -1;
		if(msg->status & MOUTGOING) {
			if(((hf = find_field(msg, REPLY_ORGMSG)) != NULL) &&
			   ((msg1 = get_msg_by_url(hf->f_line)) != NULL))
				msg1->flags |= ANSWERED;
			else {
				if(((hf = find_field(msg, FWD_ORGMSG)) != NULL) &&
				   ((msg1 = get_msg_by_url(hf->f_line)) != NULL))
					msg1->flags |= FORWARDED;
			}

			action = R_DISCARD;
		} else {
			action = R_MOVE;
			data = INBOX;
		}
	} else {
		action = rule->action;
		data = rule->data;
	}

	if(msg->status & RECENT) {
		source = get_msg_src(msg);
		if(rule) {
			if((rule->flags & R_SAVE_ADDR) ||
			   (source && (source->flags & RSRC_SAVEADDR)))
				add_msg_addr(msg, "default");

			if(rule->flags & R_MARK_READ)
				msg->flags &= ~UNREAD;

			if(rule->flags & R_SILENT)
				mcount = 1;
		} else {
			if(Config.getInt("saveaddr", 0)
			   || (source && (source->flags & RSRC_SAVEADDR)))
				add_msg_addr(msg, "default");
		}
	}

	switch(action) {
		case R_MOVE:
			folder = get_folder_by_name(data);
			if(!folder) {
				folder = inbox;
			}

			msg->status &= ~NOTINCOMING;
			msg->status &= ~MOUTGOING;

			if(delay && msg->folder) {
				msg->folder = folder;
				msg->status |= MOVED;
				break;
			}

			if(folder->move(msg, folder) == -1) {
				return -1;
			}
			break;

		case R_DISCARD:
			msg->status |= (DELETED | DELPERM);
			if(!delay)
				msg->mdelete(msg);
			break;

		case R_FORWARD:
			msg1 = get_fwd_msg(msg, NULL);
			if(!msg1)
				return -1;

			discard_address(msg1->header->To);
			addr = get_address(data, 0);
			msg1->header->News = expand_news_addr_list(addr, 1);
			msg1->header->To = expand_addr_list(msg, addr);

			if(send_message(msg1) != 0) {
				msg1->status |= (DELETED | DELPERM);
				msg1->mdelete(msg1);
			}

			if(rule->flags & R_VAC_KEEP) {
				if((rule = match_msg(msg, R_MOVE)) == NULL)
					folder = inbox;
				else {
					if((folder = get_folder_by_name(rule->data)) == NULL)
						folder = inbox;
					if(apply_rule_opts(rule, msg) == 0)
						mcount = 0;
				}

				msg->status &= ~NOTINCOMING;
				msg->status &= ~MOUTGOING;
				if(delay && msg->folder) {
					msg->folder = folder;
					msg->status |= MOVED;
				} else {
					if(folder->move(msg, folder) == -1)
						return -1;
				}
			} else {
				msg->status |= (DELETED | DELPERM);
				if(!delay)
					msg->mdelete(msg);
			}
			break;

		case R_VACATION:
			msg1 = get_vac_msg(msg, data);
			if(!msg1)
				return -1;

			if(send_message(msg1) != 0) {
				msg1->status |= (DELETED | DELPERM);
				msg1->mdelete(msg1);
			}

			if(rule->flags & R_VAC_KEEP) {
				if((rule = match_msg(msg, R_MOVE)) == NULL)
					folder = inbox;
				else {
					if((folder = get_folder_by_name(rule->data)) == NULL)
						folder = inbox;
					if(apply_rule_opts(rule, msg) == 0)
						mcount = 0;
				}

				msg->status &= ~NOTINCOMING;
				msg->status &= ~MOUTGOING;
				if(delay && msg->folder) {
					msg->folder = folder;
					msg->status |= MOVED;
				} else {
					if(folder->move(msg, folder) == -1)
						return -1;
				}
			} else {
				msg->status |= (DELETED | DELPERM);
				if(!delay)
					msg->mdelete(msg);
			}
			break;

		case R_RESEND:
			msg->status |= LOCKED;
			msg1 = outbox->copy(msg, outbox);
			msg->status &= ~LOCKED;
			msg1->status &= ~LOCKED;
			msg1->flags &= ~UNREAD;
			msg1->status |= MSGNEW;
			if(!msg1)
				return -1;

			discard_address(msg1->header->To);
			discard_address(msg->header->Cc);
			discard_address(msg->header->Bcc);

			msg->header->To = NULL;
			msg->header->Cc = NULL;
			msg->header->Bcc = NULL;

			addr = get_address(data, 0);
			msg1->header->News = expand_news_addr_list(addr, 1);
			msg1->header->To = expand_addr_list(msg, addr);

			if(send_message(msg1) != 0) {
				msg1->status |= (DELETED | DELPERM);
				msg1->mdelete(msg1);
			}

			if(rule->flags & R_VAC_KEEP) {
				if((rule = match_msg(msg, R_MOVE)) == NULL)
					folder = inbox;
				else {
					if((folder = get_folder_by_name(rule->data)) == NULL)
						folder = inbox;
					if(apply_rule_opts(rule, msg) == 0)
						mcount = 0;
				}

				msg->status &= ~NOTINCOMING;
				msg->status &= ~MOUTGOING;
				if(delay && msg->folder) {
					msg->folder = folder;
					msg->status |= MOVED;
				} else {
					if(folder->move(msg, folder) == -1)
						return -1;
				}
			} else {
				msg->status |= (DELETED | DELPERM);
				if(!delay)
					msg->mdelete(msg);
			}
			break;

		case R_EXECUTE:
			init_pinfo(&pinfo);
			pinfo.wait = WAIT_BG;
			msg->update(msg);
			if((p = msg->get_file(msg)) == NULL)
				return -1;
			snprintf(mfile, sizeof(mfile), "%s", p);

			if((pinfo.ifd = open(mfile, O_RDONLY)) <= 0)
				return -1;

			if(rule->flags & R_EMODIFY) {
				if((mnum = get_new_name(ftemp)) == -1)
					return -1;

				snprintf(buf, sizeof(buf), "%s/%d", ftemp->fold_path, mnum);
				if(
				  (pinfo.ofd =
				   open(buf, O_WRONLY | O_CREAT | O_TRUNC, 00600)) <= 0)
					return -1;
			}

			if((eres = exec_child(data, &pinfo)) < 0) {
				close(pinfo.ifd);
				if(rule->flags & R_EMODIFY) {
					close(pinfo.ofd);
					unlink(buf);
				}
				return -1;
			}

			if(rule->flags & R_EMODIFY) {
				if((eres == 0) && (stat(buf, &sb) != -1) && (sb.st_size > 0)) {
					if((msg1 = get_message(mnum, ftemp)) == NULL) {
						display_msg(MSG_WARN, "apply rule",
									"exec resulted in invalid message");
						return -1;
					}
					discard_mime(msg->mime);
					msg->mime = NULL;
					msg->free_text(msg);
					if(rename(buf, mfile) == -1) {
						display_msg(MSG_WARN, "apply rule", "rename failed");
						return -1;
					}
					discard_message_header(msg);
					msg->header = msg1->header;
					msg->msg_len = msg1->msg_len;
					msg1->header = NULL;
					discard_message(msg1);
				} else
					unlink(buf);
			}

			if(rule->flags & R_VAC_KEEP) {
				if((rule = match_msg(msg, R_MOVE)) == NULL)
					folder = inbox;
				else {
					if((folder = get_folder_by_name(rule->data)) == NULL)
						folder = inbox;
					if(apply_rule_opts(rule, msg) == 0)
						mcount = 0;
				}

				msg->status &= ~NOTINCOMING;
				msg->status &= ~MOUTGOING;
				if(delay && msg->folder) {
					msg->folder = folder;
					msg->status |= MOVED;
				} else {
					if(folder->move(msg, folder) == -1)
						return -1;
				}
			} else {
				msg->status |= (DELETED | DELPERM);
				if(!delay)
					msg->mdelete(msg);
			}
			break;

		default:
			if(delay && msg->folder) {
				msg->folder = inbox;
				msg->status |= MOVED;
				break;
			}

			msg->status &= ~NOTINCOMING;
			msg->status &= ~MOUTGOING;
			if(inbox->move(msg, inbox) == -1) {
				return -1;
			}
			break;
	}

	return mcount;
}
