/*
** Copyright 1998 - 2018 S. Varshavchik.  See COPYING for
** distribution information.
*/

#if	HAVE_CONFIG_H
#include "rfc2045_config.h"
#endif
#include	<sys/types.h>
#include	<sys/stat.h>
#include	<time.h>
#include	<stdio.h>
#include	<errno.h>
#include	<string.h>
#include	<langinfo.h>

#if	HAVE_STRINGS_H
#include	<strings.h>
#endif

#if	HAVE_LOCALE_H
#include	<locale.h>
#endif

#include	<stdlib.h>
#include	<ctype.h>
#include	<pwd.h>
#include	<fcntl.h>
#include	<signal.h>
#include	"rfc2045/rfc2045.h"
#include	"rfc2045/encode.h"   // IWYU pragma: keep
#include	"rfc822/rfc822.h"
#include	"rfc822/rfc2047.h"
#include	"rfc2045/rfc2045charset.h"
#include	<courier-unicode.h>

#if HAVE_UNISTD_H
#include	<unistd.h>
#endif
#if HAVE_SYS_WAIT_H
#include	<sys/wait.h>
#endif
#include	"numlib/numlib.h"
#include	<iostream>
#include	<algorithm>
#include	<charconv>
#include	<fstream>

#if     HAS_GETHOSTNAME
#else
int gethostname(const char *, size_t);
#endif

void usage()
{
	std::cerr <<
		"Usage: reformime [options]\n"
		"    -d - parse a delivery status notification.\n"
		"    -e - extract contents of MIME section.\n"
		"    -x - extract MIME section to a file.\n"
		"    -X - pipe MIME section to a program.\n"
		"    -i - show MIME info.\n"
		"    -V - Validate MIME correctness.\n"
		"    -s n.n.n.n[,n.n.n.n]* - specify MIME section(s).\n"
		"    -r - rewrite message, filling in missing MIME headers.\n"
		"    -r7 - also convert 8bit/raw encoding to quoted-printable, if possible.\n"
		"    -r8 - also convert quoted-printable encoding to 8bit, if possible.\n"
		"    -rU - convert quoted-printable encoding to 8bit, unconditionally.\n"
		"    -c charset - default charset for rewriting, -o, and -O.\n"
		"    -m [file] [file]... - create a MIME message digest.\n"
		"    -h \"header\" - decode RFC 2047-encoded header.\n"
		"    -o \"header\" - encode unstructured header using RFC 2047.\n"
		"    -O \"header\" - encode address list header using RFC 2047.\n\n";

	exit(1);
}

static char *tempname(const char *tempdir)
{
char	pidbuf[NUMBUFSIZE], timebuf[NUMBUFSIZE], hostnamebuf[256];
static unsigned counter=0;
time_t	t;
char	*p;

	libmail_str_pid_t(getpid(), pidbuf);
	time(&t);
	libmail_str_time_t(t, timebuf);
	hostnamebuf[sizeof(hostnamebuf)-1]=0;
	if (gethostname(hostnamebuf, sizeof(hostnamebuf)))
		hostnamebuf[0]=0;
	p=(char *)malloc(strlen(tempdir)+strlen(pidbuf)+strlen(timebuf)+
			 strlen(hostnamebuf)+100);
	if (!p)	return (0);
	sprintf(p, "%s/%s.%s-%u.%s", tempdir, timebuf, pidbuf, counter++,
		hostnamebuf);
	return (p);
}

void read_message()
{
	char	buf[BUFSIZ];
	FILE	*tempfp=0;
	ssize_t l;

	if (fseek(stdin, 0L, SEEK_END) < 0 ||
		fseek(stdin, 0L, SEEK_SET) < 0)	/* Pipe, save to temp file */
	{
		tempfp=tmpfile();

		while ((l=fread(buf, 1, sizeof(buf), stdin)) > 0)
		{
			if (fwrite(buf, l, 1, tempfp) != 1)
			{
				perror("fwrite");
				exit(1);
			}
		}

		dup2(fileno(tempfp), 0);
		fclose(tempfp);
	}
}

void print_info(const rfc2045::entity &entity,
		const std::string_view &id)
{
	std::cout << "section: " << id
		  << "\ncontent-type: " << entity.content_type.value
		  << "\n";

	auto value=entity.content_type.parameters.find("name");
	if (value != entity.content_type.parameters.end())
		std::cout << "content-name: "
			  << value->second.value_in_charset()
			  << "\n";

	std::cout << "content-transfer-encoding: "
		  << rfc2045::to_cte(entity.content_transfer_encoding)
		  << "\n";
	std::cout << "charset: " << entity.content_type_charset()
		  << "\n";

	rfc2231::header content_disposition{
		entity.content_disposition,
		true
	};

	if (content_disposition.value.size())
		std::cout << "content-disposition: "
			  << content_disposition.value << "\n";

	auto cd_value=content_disposition.parameters.find("name");
	if (cd_value != content_disposition.parameters.end())
		std::cout << "content-disposition-name: "
			  << cd_value->second.value_in_charset()
			  << "\n";

	cd_value=content_disposition.parameters.find("filename");
	if (cd_value != content_disposition.parameters.end())
		std::cout << "content-disposition-filename: "
			  << cd_value->second.value_in_charset()
			  << "\n";

	if (!entity.content_id.empty())
		std::cout << "content-id: <" << entity.content_id << ">\n";
	if (!entity.content_description.empty())
	{
		std::cout << "content-description: ";
		rfc822::display_header(
			"content-description",
			entity.content_description,
			rfc2045::default_charset,
			std::ostreambuf_iterator<char>{std::cout});
		std::cout << "\n";
	}

	std::cout << "starting-pos: " << entity.startpos << "\n";
	std::cout << "starting-pos-body: " << entity.startbody << "\n";
	std::cout << "ending-pos: " << entity.endbody << "\n";
	std::cout << "line-count: " << entity.nlines << "\n";
	std::cout << "body-line-count: " << entity.nbodylines << "\n\n";
}

void do_print_section(const rfc2045::entity &e,
		      std::streambuf &src,
		      std::streambuf &out)
{
	rfc2045::mime_decoder decoder{
		[&]
		(const char *p, size_t n)
		{
			out.sputn(p, n);
		},
		src
	};

	decoder.decode_header=false;
	decoder.decode_body=true;
	decoder.decode_subentities=false;

	decoder.decode(e);
}

void rewrite(rfc2045::entity &message, rfc822::fdstreambuf &src,
	     rfc2045::convert rwmode)
{
	auto out_p=std::cout.rdbuf();

	if (!message.autoconvert_check(rwmode))
	{
		std::copy(std::istreambuf_iterator<char>{&src},
			  std::istreambuf_iterator<char>{},
			  std::ostreambuf_iterator<char>{out_p});
		return;
	}

	rfc2045::entity::autoconvert_meta metadata;

	metadata.appid="reformime (" RFC2045PKG " " RFC2045VER ")";

	rfc2045::entity::line_iter<false>::autoconvert(
		message,
		[&]
		(const char *p, size_t n)
		{
			if (static_cast<size_t>(out_p->sputn(p, n)) != n)
			{
				perror("write");
				exit(1);
			}
		},
		src,
		metadata
	);
}

std::string get_suitable_filename(const rfc2045::entity &message,
				  std::string_view pfix,
				  bool ignore_filename)
{
	std::string filename;

	rfc2231::header content_disposition{
		message.content_disposition, true
	};

	auto cd_value=content_disposition.parameters.find("filename");
	if (cd_value != content_disposition.parameters.end())
		filename=cd_value->second.value_in_charset();

	if (filename.empty())
	{
		cd_value=content_disposition.parameters.find("name");
		if (cd_value != content_disposition.parameters.end())
			filename=cd_value->second.value_in_charset();
	}

	if (filename.empty())
	{
		auto cd_value=message.content_type.parameters.find("name");
		if (cd_value != content_disposition.parameters.end())
			filename=cd_value->second.value_in_charset();
	}

	// Strip leading/trailing spaces

	filename.erase(
		filename.begin(),
		std::find_if(filename.begin(), filename.end(),
			     [](unsigned c){ return !isspace(c);})
	);

	for (auto b=filename.begin(), e=filename.end(); b != e; --e)
	{
		unsigned char c=e[-1];

		if (!isspace(c))
		{
			filename.erase(e, filename.end());
			break;
		}
	}

	for (auto b=filename.begin(), e=filename.end(); b != e; --e)
	{
		unsigned char c=e[-1];

		if (c == '/' || c == '\\')
		{
			filename.erase(filename.begin(), e);
			break;
		}
	}

	if (filename.size()>32)
	{
		filename.erase(filename.begin(), filename.end()-32);
	}

	if (ignore_filename)
	{
		char	numbuf[sizeof(size_t)*2+1];
		static size_t counter=0;

		auto p=std::to_chars(numbuf, numbuf+sizeof(numbuf)-1,
				     ++counter, 16).ptr;

		*p++='-';

		filename.insert(filename.begin(), numbuf, p);
	}
	else if (filename.empty())
	{
		filename=tempname(".");
		filename.erase(filename.begin(),
			       filename.begin()+2);	/* Skip over ./ */
	}

	for (char &c:filename)
	{
		unsigned char d=c;
		if (!isalnum(d) && d != '.' && d != '-' && d != '=')
			c='_';
	}

	filename.insert(filename.begin(), pfix.begin(), pfix.end());

	if (pfix.size() == 0)
	{
		std::fstream tty{"/dev/tty"};

		if (!tty)
		{
			perror("/dev/tty");
			exit(1);
		}

		tty << "Extract " << message.content_type.value
		    << "? " << std::flush;

		std::string resp;

		std::getline(tty, resp);

		switch (*resp.c_str()) {
		case 'y':
		case 'Y':
			break;
		default:
			filename.clear();
			return filename;
		}

		tty.close();
		tty.open("/dev/tty");
		if (!tty)
		{
			perror("/dev/tty");
			exit(1);
		}
		tty << "Filename [" << filename << "]: " << std::flush;
		std::getline(tty, resp);

		if (!resp.empty())
			filename=resp;
	}

	return filename;
}

void extract_file(const rfc2045::entity &message,
		  std::streambuf &source,
		  std::string_view filename,
		  int argc, char **argv)
{
	bool ignore=false;
	int fd;

	for (;;)
	{
		auto f=get_suitable_filename(message, filename, ignore);

		if (f.empty())
			return;

		fd=open(f.c_str(), O_WRONLY|O_CREAT|O_EXCL, 0666);
		if (fd < 0)
		{
			if (errno == EEXIST)
			{
				std:: cout << f << " exists.\n";
				ignore=true;
				continue;
			}

			perror(f.c_str());
			exit(1);
		}
		break;
	}

	rfc822::fdstreambuf out{fd};

	do_print_section(message, source, out);

	if (out.pubsync() < 0)
	{
		perror("write");
		exit(1);
	}
}

void extract_pipe(const rfc2045::entity &message,
		  std::streambuf &source,
		  std::string_view filename,
		  int argc, char **argv)
{
	pid_t	pid, p2;
	int	waitstat;

	auto f=get_suitable_filename(message, "FILENAME=", false);

	int	pipefd[2];

	if (argc == 0)
	{
		std::cerr << "reformime: Invalid -X option.\n";
		exit(1);
	}

	if (pipe(pipefd))
	{
		perror("pipe");
		exit(1);
	}

	rfc822::fdstreambuf out{pipefd[1]};

	while ((pid=fork()) == -1)
	{
		perror("fork");
		sleep(2);
	}

	if (pid == 0)
	{
		out=rfc822::fdstreambuf{};

		putenv(const_cast<char *>(f.c_str()));

		std::string content_type_env;

		content_type_env.reserve(message.content_type.value.size()+
					 sizeof("CONTENT_TYPE=")-1);

		content_type_env="CONTENT_TYPE=";
		content_type_env += message.content_type.value;
		putenv(const_cast<char *>(content_type_env.c_str()));

		dup2(pipefd[0], 0);
		close(pipefd[0]);

		execv(argv[0], argv);
		perror("exec");
		_exit(1);
	}
	close(pipefd[0]);
	signal(SIGPIPE, SIG_IGN);
	do_print_section(message, source, out);
	signal(SIGPIPE, SIG_DFL);
	if (out.pubsync() < 0)
	{
		perror("write");
		exit(1);
	}
	out=rfc822::fdstreambuf{};

	while ((p2=wait(&waitstat)) != pid && p2 != -1)
		;

	if ((p2 == pid) && WIFEXITED(waitstat))
	{
		if (WEXITSTATUS(waitstat) != 0)
		{
			fprintf(stderr, "reformime: %s exited with status %d.\n",
				argv[0], WEXITSTATUS(waitstat));
			exit(WEXITSTATUS(waitstat) + 20);
		}
	}
}

static void for_mime_section(
	const rfc2045::entity &message,
	std::string_view mimesection,
	std::function<void (const rfc2045::entity &,
			    std::string_view)> cb)
{
	if (!mimesection.size())
	{
		message.enumerate(
			[&]
			(auto id, auto &e)
			{
				cb(e, id);
			}
		);
	}

	while (mimesection.size())
	{
		auto s=mimesection.data(),p=s;
		size_t n=mimesection.size();

		for (; n; ++p, --n)
		{
			if (*p == ',')
				break;
		}

		std::string_view this_id{
			s, static_cast<size_t>(p-s)
		};

		auto ptr=message.find(this_id);

		if (ptr)
			cb(*ptr, this_id);

		if (n)
		{
			++p;
			--n;
		}
		mimesection=std::string_view{p, n};
	}
}

static void mimedigest1(int, char **);

static void mimedigest(int argc, char **argv)
{
	std::string mimebuf;
	std::vector<std::vector<char>> filenames;

	if (argc > 0)
	{
		mimedigest1(argc, argv);
		return;
	}

	while (std::getline(std::cin, mimebuf))
	{
		std::vector<char> str;

		str.reserve(mimebuf.size()+1);
		str.insert(str.end(), mimebuf.begin(), mimebuf.end());
		str.push_back(0);

		filenames.push_back(std::move(str));
	}

	std::vector<char *> strptrs;

	strptrs.reserve(filenames.size());

	for (auto &vec:filenames)
	{
		strptrs.push_back(vec.data());
	}

	mimedigest1(strptrs.size(), strptrs.data());
}

static void mimedigest1(int argc, char **argv)
{
	std::string boundarybuf;
	unsigned boundarycnt=0;
	std::vector<bool> utf8;

	if (argc == 0)
		return;

	do
	{
		boundarybuf=rfc2045::entity::new_boundary(boundarycnt);
		utf8.clear();
		utf8.reserve(argc);

		for (int i=0; i<argc; ++i)
		{
			std::ifstream fp{argv[i]};

			if (!fp)
			{
				perror(argv[i]);
				exit(1);
			}

			rfc2045::entity message;

			{
				std::istreambuf_iterator<char> b{fp.rdbuf()}, e;
				rfc2045::entity::line_iter<false>::iter parser{
					b,
					e
				};

				message.parse(parser);

				if (message.errors.fatal())
				{
					auto errors=message.errors.describe();

					std::cout << argv[i] << ":\n";

					for (auto &message:errors)
						std::cout << "    "
							  << message
							  << "\n";

					exit(1);
				}

				utf8.push_back(
					(message.all_errors()
					 & RFC2045_ERR8BITHEADER) != 0
				);


				if (rfc2045::entity::line_iter<false>
				    ::try_boundary(*fp.rdbuf(),
						   boundarybuf,
						   message))
				{
					boundarybuf.clear();
					break;
				}
			}
		}
	} while (boundarybuf.empty());

	std::cout << "Mime-Version:1.0\n"
		"Content-Type: multipart/digest; boundary=\""
		  << boundarybuf << "\"\n\n"
		RFC2045MIMEMSG;

	for (int i=0; i<argc; i++)
	{
		std::ifstream fp{argv[i]};

		if (!fp)
		{
			perror(argv[i]);
			exit(1);
		}

		std::cout << "\n--"
			  << boundarybuf << "\nContent-Type: "
			  << (utf8[i] ? RFC2045_MIME_MESSAGE_GLOBAL "\n\n":
			      RFC2045_MIME_MESSAGE_RFC822 "\n\n"
			  )
			  << fp.rdbuf();
	}
	std::cout << "\n--" << boundarybuf << "--\n";
}

static int main2(const char *mimecharset, int argc, char **argv)
{
	int	argn;
	char	optc;
	char	*optarg;
	std::string_view mimesection;
	bool dorewrite{false}, doinfo{false}, dodecode{false},
		domimedigest{false}, dodecodehdr{false},
		dodecodeaddrhdr{false}, doencodemime{false},
		doencodemimehdr{false}, convtoutf8{false};
	int	dodsn=0;

	const char	*decode_header="";
	rfc2045::convert rwmode{rfc2045::convert::standardize};
	bool	dovalidate{false};
	void	(*do_extract)(const rfc2045::entity &message,
			      std::streambuf &source,
			      std::string_view filename,
			      int argc, char **argv)=nullptr;

	std::string_view extract_filename;
	int rc=0;

	for (argn=1; argn<argc; )
	{
		if (argv[argn][0] != '-')	break;
		optarg=0;
		optc=argv[argn][1];
		if (optc && argv[argn][2])	optarg=argv[argn]+2;
		++argn;
		switch	(optc)	{
		case 'c':
			if (!optarg && argn < argc)
				optarg=argv[argn++];
			if (optarg && *optarg)
			{
				char *p=unicode_convert_tobuf("",
								optarg,
								unicode_u_ucs4_native,
								NULL);

				if (!p)
				{
					fprintf(stderr, "Unknown charset: %s\n",
						optarg);
					exit(1);
				}
				free(p);
				mimecharset=optarg;
			}
			break;

		case 's':
			if (!optarg && argn < argc)
				optarg=argv[argn++];
			if (optarg && *optarg)	mimesection=optarg;
			break;
		case 'i':
			doinfo=true;
			break;
		case 'e':
			dodecode=true;
			break;
		case 'r':
			dorewrite=true;
			if (optarg && *optarg == '7')
				rwmode=rfc2045::convert::sevenbit;
			if (optarg && *optarg == '8')
				rwmode=rfc2045::convert::eightbit;;
			if (optarg && *optarg == 'U')
				rwmode=rfc2045::convert::eightbit_always;
			break;
		case 'm':
			domimedigest=true;
			break;
		case 'd':
			dodsn=1;
			break;
		case 'D':
			dodsn=2;
			break;
		case 'x':
			do_extract=extract_file;
			if (optarg)
				extract_filename=optarg;
			break;
		case 'X':
			do_extract=extract_pipe;
			break;
		case 'V':
			dovalidate=true;
			break;
		case 'h':
			if (!optarg && argn < argc)
				optarg=argv[argn++];
			if (optarg)
			{
				decode_header=optarg;
			}
			dodecodehdr=true;
			break;
		case 'H':
			if (!optarg && argn < argc)
				optarg=argv[argn++];
			if (optarg)
			{
				decode_header=optarg;
			}
			dodecodeaddrhdr=true;
			break;
		case 'o':
			if (!optarg && argn < argc)
				optarg=argv[argn++];
			if (optarg)
			{
				decode_header=optarg;
			}
			doencodemime=true;
			break;
		case 'O':
			if (!optarg && argn < argc)
				optarg=argv[argn++];
			if (optarg)
			{
				decode_header=optarg;
			}
			doencodemimehdr=true;
			break;
		case 'u':
			convtoutf8=true;
			break;
		default:
			usage();
		}
	}

	rfc2045::default_charset=mimecharset;

	if (domimedigest)
	{
		mimedigest(argc-argn, argv+argn);
		return (0);
	}
	else if (dodecodehdr)
	{
		rfc822::display_header("Subject",
				       decode_header,
				       mimecharset,
				       std::ostreambuf_iterator<char>{
					       std::cout.rdbuf()
				       });

		std::cout << "\n";
		return (0);
	}
	else if (dodecodeaddrhdr)
	{
		rfc822::display_header("To",
				       decode_header,
				       mimecharset,
				       std::ostreambuf_iterator<char>{
					       std::cout.rdbuf()
				       },
				       []
				       {
					       std::cout << "\n";
				       }
		);

		std::cout << "\n";
		return (0);
	}

	if (doencodemime)
	{
		std::string_view hdr{decode_header};

		const auto &[str, error]=rfc2047::encode(
			hdr.begin(),
			hdr.end(),
			mimecharset, rfc2047::qp_allow_any);

		if (!error)
		{
			std::cout << str << "\n";
		}
		return (0);
	}
	if (doencodemimehdr)
	{
		rfc822::tokens t{decode_header};
		rfc822::addresses a{t};

		std::string s;

		a.encode(std::back_inserter(s), mimecharset,
			 [&]
			 (const char *sep)
			 {
				 while (*sep)
				 {
					 if (*sep == ' ')
					 {
						 s += "\n ";
					 }
					 s.push_back(*sep);
					 ++sep;
				 }
			 }
		);

		printf("%s\n", s.c_str());
		return (0);
	}

	read_message();

	rfc2045::entity message;

	int fd=dup(0);

	if (fd < 0)
	{
		perror("dup");
		exit(1);
	}

	rfc822::fdstreambuf src{fd};

	if (src.pubseekpos(0) < 0)
	{
		perror("seek");
		exit(1);
	}

	{
		std::istreambuf_iterator<char> b{&src}, e;
		rfc2045::entity::line_iter<false>::iter parser{b, e};
		message.parse(parser);
		if (src.pubseekpos(0) < 0)
		{
			perror("seek");
			exit(1);
		}

		if (message.errors.fatal())
		{
			auto errors=message.errors.describe();

			std::cout << "Input message:\n";

			for (auto &message:errors)
				std::cout << "    "
					  << message
					  << "\n";

			exit(1);
		}
	}

	if (doinfo || do_extract || dodecode)
	{
		for_mime_section(
			message, mimesection,
			[&]
			(const rfc2045::entity &e,
			 std::string_view id)
			{
				if (doinfo)
				{
					print_info(e, id);
				}

				if (dodecode)
				{
					if (!e.subentities.size())
					{
						do_print_section(
							e, src,
							*std::cout.rdbuf());
						return;
					}
				}
				else
				{
					if (!do_extract)
						return;

					if (!e.subentities.size())
					{
						do_extract(
							e,
							src,
							extract_filename,
							argc-argn,
							argv+argn
						);
						return;
					}
				}

				if (mimesection.size())
				{
					std::cerr
						<< e.content_type.value
						<< " ("
						<< id
						<< ") cannot be extracted.\n";
					rc=1;
				}
			}
		);
	}
	else if (dorewrite)
		rewrite(message, src, rwmode);
	else if (dodsn)
		message.getdsn(
			src,
			[&]
			(const rfc2045::entity::dsn &dsn)
			{
				auto &addr= dodsn == 2 &&
					!dsn.original_recipient.empty() ?
					dsn.original_recipient:
					dsn.final_recipient;

				if (addr.empty())
					return;

				std::cout << dsn.action << " " << addr << "\n";
			});
	else if (dovalidate)
	{
		for_mime_section(
			message,
			"",
			[&]
			(const rfc2045::entity &e,
			 const std::string_view &id)
			{
				auto errors=e.errors.describe();

				if (e.errors.fatal())
				{
					std::cout << "Fatal error ("
						  << id <<"):\n";
					rc=1;
				}
				else if (!errors.empty())
				{
					std::cout << "Warnings ("
						  << id << "):\n";
				}

				for (auto &msg:errors)
					std::cout << "    " << msg
						  << "\n";
			});
	}
	else if (convtoutf8)
	{
		rfc2045::mime_decoder decoder{
			[]
			(const char *p, size_t n)
			{
				std::cout.write(p, n);
			},
			src,
			unicode::utf_8
		};

		decoder.add_eol=true;
		decoder.header_name_lc=false;

		decoder.decode(message);
	}
	else
	{
		message.enumerate(
			[]
			(auto id, const auto &)
			{
				std::cout << id << "\n";
			}
		);
	}

	if (std::cout.rdbuf()->pubsync() < 0)
	{
		perror("write");
		rc=1;
	}
	exit(rc);
	return (rc);
}

int main(int argc, char **argv)
{
	int	rc;

	rc=main2(unicode_default_chset(), argc, argv);
	return rc;
}
