text: /* Copyright (C) 2004, 2005 Morten K. Poulsen <morten at afdelingp.dk>
   *
   * $Id$
   *
   * Permission is hereby granted, free of charge, to any person obtaining a copy
   * of this software and associated documentation files (the "Software"), to
   * deal in the Software without restriction, including without limitation the
   * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
   * sell copies of the Software, and to permit persons to whom the Software is
   * furnished to do so, subject to the following conditions:
   *
   * The above copyright notice and this permission notice shall be included in
   * all copies or substantial portions of the Software.
   *
   * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
   * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
   * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
   * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
   * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
   * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
   * IN THE SOFTWARE.
   */
  
  #include <unistd.h>
  #include <string.h>
  #include <stdio.h>
  #include <fcntl.h>
  #include <stdlib.h>
  #include <sys/types.h>
  #include <sys/stat.h>
  #include <sys/wait.h>
  #include <ctype.h>
  
  #include "mlmmj.h"
  #include "send_digest.h"
  #include "log_error.h"
  #include "strgen.h"
  #include "memory.h"
  #include "getlistaddr.h"
  #include "getlistdelim.h"
  #include "wrappers.h"
  #include "prepstdreply.h"
  #include "mygetline.h"
  #include "gethdrline.h"
  #include "statctrl.h"
  #include "unistr.h"
  
  
  struct mail {
  	int idx;
  	char *from;
  };
  
  struct thread {
  	char *subject;
  	int num_mails;
  	struct mail *mails;
  };
  
  
  struct thread_list_state;
  typedef struct thread_list_state thread_list_state;
  struct thread_list_state {
  	const char *listdir;
  	int firstindex;
  	int lastindex;
  	int num_threads;
  	struct thread *threads;
  	int cur_thread;
  	int cur_mail;
  };
  
  
  static thread_list_state *init_thread_list(
  		const char *listdir, int firstindex, int lastindex)
  {
  	/* We use a static variable rather than dynamic allocation as
  	 * there will never be two lists in use simultaneously */
  	static thread_list_state s;
  	s.listdir = listdir;
  	s.firstindex = firstindex;
  	s.lastindex = lastindex;
  	s.num_threads = 0;
  	s.threads = NULL;
  	s.cur_thread = -1;
  	return &s;
  }
  
  
  static void rewind_thread_list(void * state)
  {
  	thread_list_state *s = (thread_list_state *)state;
  	int i, j, archivefd, thread_idx;
  	char *line, *tmp, *subj, *from;
  	char *archivename;
  	int num_threads = 0;
  	struct thread *threads = NULL;
  	char buf[45];
  
  	if (s->cur_thread != -1) {
  		/* We have gathered the data already; just rewind */
  		s->cur_thread = 0;
  		s->cur_mail = -1;
  		return;
  	}
  
  	for (i=s->firstindex; i<=s->lastindex; i++) {
  
  		snprintf(buf, sizeof(buf), "%d", i);
  
  		archivename = concatstr(3, s->listdir, "/archive/", buf);
  		archivefd = open(archivename, O_RDONLY);
  		myfree(archivename);
  
  		if (archivefd < 0)
  			continue;
  
  		subj = NULL;
  		from = NULL;
  
  		while ((line = gethdrline(archivefd, NULL))) {
  			if (strncasecmp(line, "Subject:", 8) == 0) {
  				myfree(subj);
  				subj = unistr_header_to_utf8(line + 8);
  			}
  			if (strncasecmp(line, "From:", 5) == 0) {
  				myfree(from);
  				from = unistr_header_to_utf8(line + 5);
  			}
  			myfree(line);
  		}
  
  		if (!subj) {
  			subj = mystrdup("no subject");
  		}
  
  		if (!from) {
  			from = mystrdup("anonymous");
  		}
  
  		tmp = subj;
  		for (;;) {
  			if (isspace(*tmp)) {
  				tmp++;
  				continue;
  			}
  			if (strncasecmp(tmp, "Re:", 3) == 0) {
  				tmp += 3;
  				continue;
  			}
  			break;
  		}
  		/* tmp is now the clean subject */
  
  		thread_idx = -1;
  		for (j=0; j<num_threads; j++) {
  			if (strcmp(subj, threads[j].subject) == 0) {
  				thread_idx = j;
  				break;
  			}
  		}
  		if (thread_idx == -1) {
  			num_threads++;
  			threads = myrealloc(threads,
  					num_threads*sizeof(struct thread));
  			threads[num_threads-1].subject = concatstr(2,tmp,"\n");
  			threads[num_threads-1].num_mails = 0;
  			threads[num_threads-1].mails = NULL;
  			thread_idx = num_threads-1;
  		}
  
  		tmp = from;
  		for (;;) {
  			if (isspace(*tmp)) {
  				tmp++;
  				continue;
  			}
  			break;
  		}
  		/* tmp is now left-trimmed from */
  
  		threads[thread_idx].num_mails++;
  		threads[thread_idx].mails = myrealloc(threads[thread_idx].mails,
  				threads[thread_idx].num_mails*sizeof(struct mail));
  		threads[thread_idx].mails[threads[thread_idx].num_mails-1].idx = i;
  		threads[thread_idx].mails[threads[thread_idx].num_mails-1].from =
  				concatstr(5, "      ", buf, " - ", tmp, "\n");
  
  		myfree(subj);
  		myfree(from);
  
  		close(archivefd);
  	}
  
  	s->num_threads = num_threads;
  	s->threads = threads;
  	s->cur_thread = 0;
  	s->cur_mail = -1;
  }
  
  
  static const char *get_thread_list_line(void * state)
  {
  	thread_list_state *s = (thread_list_state *)state;
  
  	if (s->cur_thread >= s->num_threads) return NULL;
  
  	if (s->cur_mail == -1) {
  		s->cur_mail = 0;
  		return s->threads[s->cur_thread].subject;
  	}
  
  	if (s->cur_mail >= s->threads[s->cur_thread].num_mails) {
  		s->cur_thread++;
  		s->cur_mail = -1;
  		return "\n";
  	}
  
  	return s->threads[s->cur_thread].mails[s->cur_mail++].from;
  }
  
  
  static void finish_thread_list(thread_list_state * s)
  {
  	int i, j;
  	if (s->threads == NULL) return;
  	for (i=0; i<s->num_threads; i++) {
  		myfree(s->threads[i].subject);
  		for (j=0; j<s->threads[i].num_mails; j++) {
  			myfree(s->threads[i].mails[j].from);
  		}
  		myfree(s->threads[i].mails);
  	}
  	myfree(s->threads);
  	s->threads = NULL;
  }
  
  
  int send_digest(const char *listdir, int firstindex, int lastindex,
  		int issue, const char *addr, const char *mlmmjsend)
  {
  	int i, fd, archivefd, status, hdrfd;
  	size_t len;
  	text * txt;
  	char buf[100];
  	char *tmp, *queuename = NULL, *archivename, *subject = NULL, *line = NULL;
  	char *boundary, *listaddr, *listdelim, *listname, *listfqdn;
  	pid_t childpid, pid;
  	thread_list_state * tls;
  
  	if (addr) {
  		errno = 0;
  		log_error(LOG_ARGS, "send_digest() does not support sending "
  				"digest mails to only one recipient yet");
  		return -1;
  	}
  
  	if (firstindex > lastindex)
  		return -1;
  	
  	do {
  		tmp = random_str();
  		myfree(queuename);
  		queuename = concatstr(3, listdir, "/queue/", tmp);
  		myfree(tmp);
  		fd = open(queuename, O_RDWR|O_CREAT|O_EXCL, S_IRUSR|S_IWUSR);
  	} while ((fd < 0) && (errno == EEXIST));
  
  	if (fd < 0) {
  		log_error(LOG_ARGS, "Could not open digest queue file '%s'",
  				queuename);
  		myfree(queuename);
  		return -1;
  	}
  
  	tmp = concatstr(2, listdir, "/control/customheaders");
  	hdrfd = open(tmp, O_RDONLY);
  	myfree(tmp);
  
  	boundary = random_str();
  
  	listaddr = getlistaddr(listdir);
  	listname = genlistname(listaddr);
  	listfqdn = genlistfqdn(listaddr);
  	listdelim = getlistdelim(listdir);
  	
  	txt = open_text_file(listdir, "digest");
  	if (txt == NULL) {
  		log_error(LOG_ARGS, "Could not open listtext 'digest'");
  		goto fallback_subject;
  	}
  
  	snprintf(buf, sizeof(buf), "%d", firstindex);
  	register_unformatted(txt, "digestfirst", buf);
  
  	snprintf(buf, sizeof(buf), "%d", lastindex);
  	register_unformatted(txt, "digestlast", buf);
  
  	if (lastindex == firstindex) {
  		snprintf(buf, sizeof(buf), "%d", firstindex);
  	} else {
  		snprintf(buf, sizeof(buf), "%d-%d", firstindex, lastindex);
  	}
  	register_unformatted(txt, "digestinterval", buf);
  
  	snprintf(buf, sizeof(buf), "%d", issue);
  	register_unformatted(txt, "digestissue", buf);
  
  	register_unformatted(txt, "digestthreads", "%digestthreads%"); /* DEPRECATED */
  
  	tls = init_thread_list(listdir, firstindex, lastindex);
  	register_formatted(txt, "digestthreads", rewind_thread_list,
  			get_thread_list_line, tls);
  
  	line = get_processed_text_line(txt, 1, listaddr, listdelim, listdir);
  
  	if (line == NULL) {
  		log_error(LOG_ARGS, "No content in digest listtext");
  		goto fallback_subject;
  	}
  
  	tmp = line;
  	len = 0;
  	while (*tmp && *tmp != ':') {
  		tmp++;
  		len++;
  	}
  	if (!*tmp) {
  		log_error(LOG_ARGS, "No subject or invalid "
  				"subject in digest listtext");
  		goto fallback_subject;
  	}
  	tmp++;
  	len++;
  	if (strncasecmp(line, "Subject:", len) == 0) {
  		tmp = unistr_utf8_to_header(tmp);
  		subject = concatstr(2, "Subject: ", tmp);
  		myfree(tmp);
  		myfree(line);
  
  		/* Skip the empty line after the subject */
  		line = get_processed_text_line(txt, 1, listaddr, listdelim,
  				listdir);
  		if (line == NULL || *line != '\0') {
  			log_error(LOG_ARGS, "Too many headers "
  					"in digest listtext");
  			goto fallback_subject;
  		}
  
  		if (line != NULL) myfree(line);
  		line = NULL;
  	} else {
  		log_error(LOG_ARGS, "No subject or invalid "
  				"subject in digest listtext");
  		goto fallback_subject;
  	}
  
  fallback_subject:
  	if (subject == NULL) {
  		if (lastindex == firstindex) {
  			snprintf(buf, sizeof(buf), "%d", firstindex);
  		} else {
  			snprintf(buf, sizeof(buf), "%d-%d", firstindex, lastindex);
  		}
  		tmp = mystrdup(buf);
  		snprintf(buf, sizeof(buf), "Digest of %s issue %d (%s)",
  				listaddr, issue, tmp);
  		subject = unistr_utf8_to_header(buf);
  		myfree(tmp);
  	}
  
  	tmp = concatstr(10, "From: ", listname, listdelim, "help@", listfqdn,
  			   "\nMIME-Version: 1.0"
  			   "\nContent-Type: multipart/" DIGESTMIMETYPE "; "
  			   "boundary=", boundary,
  			   "\nSubject: ", subject,
  			   "\n");
  
  	myfree(listfqdn);
  	myfree(subject);
  
  	if (writen(fd, tmp, strlen(tmp)) < 0) {
  		myfree(tmp);
  		goto errdighdrs;
  	}
  	myfree(tmp);
  
  	if(hdrfd >= 0 && dumpfd2fd(hdrfd, fd) < 0) {
  		goto errdighdrs;
  	}
  
  	close(hdrfd);
  	hdrfd = -1;
  
  	if (writen(fd, "\n", 1) < 0) {
  errdighdrs:
  		log_error(LOG_ARGS, "Could not write digest headers to '%s'",
  				queuename);
  		close(fd);
  		unlink(queuename);
  		myfree(boundary);
  		myfree(queuename);
  		myfree(listaddr);
  		myfree(listname);
  		myfree(listdelim);
  		if (txt != NULL) {
  			close_text(txt);
  			myfree(line);
  		}
  		if (hdrfd > 0) {
  			close(hdrfd);
  		}
  		return -1;
  	}
  
  	if ((txt != NULL) && !statctrl(listdir, "nodigesttext")) {
  
  		tmp = concatstr(3, "\n--", boundary,
  				"\nContent-Type: text/plain; charset=UTF-8"
  				"\nContent-Transfer-Encoding: 8bit"
  				"\n\n");
  		if (writen(fd, tmp, strlen(tmp)) == -1) {
  			log_error(LOG_ARGS, "Could not write digest text/plain"
  					" part headers to '%s'", queuename);
  			close(fd);
  			unlink(queuename);
  			myfree(boundary);
  			myfree(tmp);
  			myfree(queuename);
  			myfree(listaddr);
  			myfree(listname);
  			myfree(listdelim);
  			if (txt != NULL) {
  				close_text(txt);
  				myfree(line);
  			}
  			return -1;
  		}
  		myfree(tmp);
  
  		for (;;) {
  			line = get_processed_text_line(txt, 0, listaddr, listdelim,
  					listdir);
  			if (line == NULL) break;
  			len = strlen(line);
  			line[len] = '\n';
  			if(writen(fd, line, len+1) < 0) {
  				myfree(line);
  				log_error(LOG_ARGS, "Could not write"
  						" std mail");
  				break;
  			}
  			myfree(line);
  		}
  
  		finish_thread_list(tls);
  		close_text(txt);
  	} else if (txt != NULL) {
  		finish_thread_list(tls);
  		close_text(txt);
  	}
  
  	if (line != NULL) myfree(line);
  	myfree(listaddr);
  	myfree(listdelim);
  
  	for (i=firstindex; i<=lastindex; i++) {
  		snprintf(buf, sizeof(buf), "%d", i);
  		
  		archivename = concatstr(3, listdir, "/archive/", buf);
  		archivefd = open(archivename, O_RDONLY);
  		myfree(archivename);
  		
  		if (archivefd < 0)
  			continue;
  		
  		tmp = concatstr(7, "\n--", boundary,
  				"\nContent-Type: message/rfc822"
  				"\nContent-Disposition: inline; filename=\"",
  					listname, "_", buf, ".eml\""
  				"\n\n");
  		if (writen(fd, tmp, strlen(tmp)) == -1) {
  			log_error(LOG_ARGS, "Could not write digest part "
  					"headers for archive index %d to "
  					"'%s'", i, queuename);
  			close(fd);
  			close(archivefd);
  			unlink(queuename);
  			myfree(boundary);
  			myfree(tmp);
  			myfree(queuename);
  			myfree(listname);
  			return -1;
  		}
  		myfree(tmp);
  
  		if (dumpfd2fd(archivefd, fd) < 0) {
  			log_error(LOG_ARGS, "Could not write digest part %d "
  					"to '%s'", i,
  					queuename);
  			close(fd);
  			close(archivefd);
  			unlink(queuename);
  			myfree(boundary);
  			myfree(queuename);
  			myfree(listname);
  			return -1;
  		}
  		
  		close(archivefd);
  	}
  
  	tmp = concatstr(3, "\n--", boundary, "--\n");
  	if (writen(fd, tmp, strlen(tmp)) == -1) {
  		log_error(LOG_ARGS, "Could not write digest end to '%s'",
  				queuename);
  		close(fd);
  		unlink(queuename);
  		myfree(boundary);
  		myfree(queuename);
  		myfree(listname);
  		return -1;
  	}
  
  	close(fd);
  	myfree(boundary);
  	myfree(listname);
  	myfree(tmp);
  
  	childpid = fork();
  
  	if(childpid < 0) {
  		log_error(LOG_ARGS, "Could not fork");
  		myfree(queuename);
  		return -1;
  	}
  
  	if(childpid > 0) {
  		do /* Parent waits for the child */
  		      pid = waitpid(childpid, &status, 0);
  		while(pid == -1 && errno == EINTR);
  	} else {
  		execlp(mlmmjsend, mlmmjsend,
  				"-l", "7",
  				"-L", listdir,
  				"-m", queuename,
  				(char *)NULL);
  		log_error(LOG_ARGS, "execlp() of '%s' failed", mlmmjsend);
  		exit(EXIT_FAILURE);  /* It is OK to exit, as this is a child */
  	}
  
  	myfree(queuename);
  
  	return 0;
  }
  