// SPDX-FileCopyrightText: 2000-2002 Michael R. Elkins <me@mutt.org>
// SPDX-FileCopyrightText: 2002-2022 Oswald Buddenhagen <ossi@users.sf.net>
// SPDX-License-Identifier: GPL-2.0-or-later WITH LicenseRef-isync-GPL-exception
/*
 * mbsync - mailbox synchronizer
 */

#include "main_p.h"

#include <fcntl.h>
#include <signal.h>
#include <sys/wait.h>
#ifdef __linux__
# include <sys/prctl.h>
#endif

static void ATTR_NORETURN
version( void )
{
	puts( PACKAGE " " VERSION );
	exit( 0 );
}

static void ATTR_NORETURN
usage( int code )
{
	fputs(
PACKAGE " " VERSION " - mailbox synchronizer\n"
"Copyright (C) 2000-2002 Michael R. Elkins <me@mutt.org>\n"
"Copyright (C) 2002-2022 Oswald Buddenhagen <ossi@users.sf.net>\n"
"Copyright (C) 2004 Theodore Ts'o <tytso@mit.edu>\n"
"usage:\n"
" " EXE " [flags] {{channel[:box,...]|group} ...|-a}\n"
"  -a, --all		operate on all defined channels\n"
"  -l, --list		list mailboxes instead of syncing them\n"
"  -ls, --list-stores	raw listing of stores' mailboxes\n"
"  -n, --new		propagate new messages\n"
"  -g, --gone		propagate message disappearances (expunges)\n"
"  -f, --flags		propagate message flag changes\n"
"  -u, --upgrade		upgrade placeholders to full messages\n"
"  -L, --pull		propagate from far to near side\n"
"  -H, --push		propagate from near to far side\n"
"  -C, --create		propagate creations of mailboxes\n"
"  -R, --remove		propagate deletions of mailboxes\n"
"  -X, --expunge		expunge deleted messages\n"
"  -x, --expunge-solo	expunge deleted messages that are not paired\n"
"  -c, --config CONFIG	read an alternate config file (default: ~/." EXE "rc)\n"
"  -D, --debug		debugging modes (see manual)\n"
"  -y, --dry-run		do not actually modify anything\n"
"  -e, --ext-exit	return extended exit code\n"
"  -V, --verbose		display what is happening\n"
"  -q, --quiet		don't display progress counters\n"
"  -v, --version		display version\n"
"  -h, --help		display this help message\n"
"\nIf neither --pull nor --push are specified, both are active.\n"
"If neither --new, --gone, --flags, nor --upgrade are specified, all are\n"
"active. Direction and operation can be concatenated like --pull-new, etc.\n"
"--create, --remove, --expunge, and --expunge-solo can be suffixed with"
"-far/-near.\n"
"See the man page for details.\n"
"\nSupported mailbox formats are: IMAP4rev1, Maildir\n"
"\nCompile time options:\n"
#ifdef HAVE_LIBSSL
"  +HAVE_LIBSSL"
#else
"  -HAVE_LIBSSL"
#endif
#ifdef HAVE_LIBSASL
" +HAVE_LIBSASL"
#else
" -HAVE_LIBSASL"
#endif
#ifdef HAVE_LIBZ
" +HAVE_LIBZ"
#else
" -HAVE_LIBZ"
#endif
#ifdef USE_DB
" +USE_DB"
#else
" -USE_DB"
#endif
#ifdef HAVE_IPV6
" +HAVE_IPV6\n"
#else
" -HAVE_IPV6\n"
#endif
	, code ? stderr : stdout );
	exit( code );
}

static int child_pipe[2];
static notifier_t child_notifier;

static void
childHandler( int n ATTR_UNUSED )
{
	// We can't just reap everything here, as we might steal children
	// from popen(). Let the main loop handle it synchronously instead.
	char dummy = 0;
	write( child_pipe[1], &dummy, 1 );
}

static void
childReaper( int events ATTR_UNUSED, void *aux ATTR_UNUSED )
{
	char dummy;
	while (read( child_pipe[0], &dummy, 1 ) == 1) {}
	while (waitpid( -1, NULL, WNOHANG ) > 0) {}
}

#ifdef __linux__
static void ATTR_NORETURN
crashHandler( int n )
{
	int dpid;
	char pbuf[10], pabuf[20];

	close( 0 );
	open( "/dev/tty", O_RDWR );
	dup2( 0, 1 );
	dup2( 0, 2 );
	error( "*** " EXE " caught signal %d. Starting debugger ...\n", n );
#ifdef PR_SET_PTRACER
	int pip[2];
	if (pipe( pip ) < 0) {
		perror( "pipe()" );
		exit( 3 );
	}
#endif
	switch ((dpid = fork())) {
	case -1:
		perror( "fork()" );
		break;
	case 0:
#ifdef PR_SET_PTRACER
		close( pip[1] );
		read( pip[0], pbuf, 1 );
		close( pip[0] );
#endif
		sprintf( pbuf, "%d", Pid );
		sprintf( pabuf, "/proc/%d/exe", Pid );
		execlp( "gdb", "gdb", pabuf, pbuf, (char *)0 );
		perror( "execlp()" );
		_exit( 1 );
	default:
#ifdef PR_SET_PTRACER
		prctl( PR_SET_PTRACER, (ulong)dpid );
		close( pip[1] );
		close( pip[0] );
#endif
		waitpid( dpid, NULL, 0 );
		break;
	}
	exit( 3 );
}
#endif

void
countStep( void )
{
	if (!--JLimit)
		exit( 100 );
}

int
main( int argc, char **argv )
{
	core_vars_t mvars[1];
	char *config = NULL, *opt, *ochar;
	int oind, cops = 0, op, ms_warn = 0, renew_warn = 0, delete_warn = 0;

	tzset();
	init_timers();
	gethostname( Hostname, sizeof(Hostname) );
	if ((ochar = strchr( Hostname, '.' )))
		*ochar = 0;
	Pid = getpid();
	if (!(Home = getenv("HOME"))) {
		fputs( "Fatal: $HOME not set\n", stderr );
		return 1;
	}
	arc4_init();

	memset( mvars, 0, sizeof(*mvars) );

	for (oind = 1, ochar = NULL; ; ) {
		if (!ochar || !*ochar) {
			if (oind >= argc)
				break;
			if (argv[oind][0] != '-')
				break;
			if (argv[oind][1] == '-') {
				opt = argv[oind++] + 2;
				if (!*opt)
					break;
				if (!strcmp( opt, "config" )) {
					if (oind >= argc) {
						error( "--config requires an argument.\n" );
						return 1;
					}
					config = argv[oind++];
				} else if (starts_with( opt, -1, "config=", 7 )) {
					config = opt + 7;
				} else if (!strcmp( opt, "all" )) {
					mvars->all = 1;
				} else if (!strcmp( opt, "list" )) {
					mvars->list = 1;
				} else if (!strcmp( opt, "list-stores" )) {
					mvars->list_stores = 1;
				} else if (!strcmp( opt, "help" )) {
					usage( 0 );
				} else if (!strcmp( opt, "version" )) {
					version();
				} else if (!strcmp( opt, "quiet" )) {
					if (Verbosity > VERYQUIET)
						Verbosity--;
				} else if (!strcmp( opt, "verbose" )) {
					Verbosity = VERBOSE;
				} else if (starts_with( opt, -1, "debug", 5 )) {
					opt += 5;
					if (!*opt)
						op = DEBUG_ALL;
					else if (!strcmp( opt, "-crash" ))
						op = DEBUG_CRASH;
					else if (!strcmp( opt, "-driver" ))
						op = DEBUG_DRV;
					else if (!strcmp( opt, "-driver-all" ))
						op = DEBUG_DRV | DEBUG_DRV_ALL;
					else if (!strcmp( opt, "-maildir" ))
						op = DEBUG_MAILDIR;
					else if (!strcmp( opt, "-main" ))
						op = DEBUG_MAIN;
					else if (!strcmp( opt, "-net" ))
						op = DEBUG_NET;
					else if (!strcmp( opt, "-net-all" ))
						op = DEBUG_NET | DEBUG_NET_ALL;
					else if (!strcmp( opt, "-sync" ))
						op = DEBUG_SYNC;
					else
						goto badopt;
					DFlags |= op;
				} else if (!strcmp( opt, "dry-run" )) {
					DFlags |= DRYRUN;
				} else if (!strcmp( opt, "ext-exit" )) {
					DFlags |= EXT_EXIT;
				} else if (!strcmp( opt, "pull" )) {
					cops |= XOP_PULL, mvars->ops[F] |= XOP_HAVE_TYPE;
				} else if (!strcmp( opt, "push" )) {
					cops |= XOP_PUSH, mvars->ops[F] |= XOP_HAVE_TYPE;
				} else if (starts_with( opt, -1, "create", 6 )) {
					opt += 6;
					op = OP_CREATE | XOP_HAVE_CREATE;
				  lcop:
					if (!*opt)
						cops |= op;
					else if (!strcmp( opt, "-far" ))
						mvars->ops[F] |= op;
					else if (!strcmp( opt, "-master" ))  // Pre-1.4 legacy
						mvars->ops[F] |= op, ms_warn = 1;
					else if (!strcmp( opt, "-near" ))
						mvars->ops[N] |= op;
					else if (!strcmp( opt, "-slave" ))  // Pre-1.4 legacy
						mvars->ops[N] |= op, ms_warn = 1;
					else
						goto badopt;
					mvars->ops[F] |= op & (XOP_HAVE_CREATE | XOP_HAVE_REMOVE | XOP_HAVE_EXPUNGE | XOP_HAVE_EXPUNGE_SOLO);
				} else if (starts_with( opt, -1, "remove", 6 )) {
					opt += 6;
					op = OP_REMOVE | XOP_HAVE_REMOVE;
					goto lcop;
				} else if (starts_with( opt, -1, "expunge-solo", 12 )) {
					opt += 12;
					op = OP_EXPUNGE_SOLO | XOP_HAVE_EXPUNGE_SOLO;
					goto lcop;
				} else if (starts_with( opt, -1, "expunge", 7 )) {
					opt += 7;
					op = OP_EXPUNGE | XOP_HAVE_EXPUNGE;
					goto lcop;
				} else if (!strcmp( opt, "no-expunge-solo" )) {
					mvars->ops[F] |= XOP_EXPUNGE_SOLO_NOOP | XOP_HAVE_EXPUNGE_SOLO;
				} else if (!strcmp( opt, "no-expunge" )) {
					mvars->ops[F] |= XOP_EXPUNGE_NOOP | XOP_HAVE_EXPUNGE;
				} else if (!strcmp( opt, "no-create" )) {
					mvars->ops[F] |= XOP_CREATE_NOOP | XOP_HAVE_CREATE;
				} else if (!strcmp( opt, "no-remove" )) {
					mvars->ops[F] |= XOP_REMOVE_NOOP | XOP_HAVE_REMOVE;
				} else if (!strcmp( opt, "noop" )) {
					mvars->ops[F] |= XOP_TYPE_NOOP | XOP_HAVE_TYPE;
				} else if (starts_with( opt, -1, "pull", 4 )) {
					op = XOP_PULL;
				  lcac:
					opt += 4;
					if (!*opt) {
						cops |= op;
					} else if (*opt == '-') {
						opt++;
						goto rlcac;
					} else {
						goto badopt;
					}
				} else if (starts_with( opt, -1, "push", 4 )) {
					op = XOP_PUSH;
					goto lcac;
				} else {
					op = 0;
				  rlcac:
					if (!strcmp( opt, "new" )) {
						op |= OP_NEW;
					} else if (!strcmp( opt, "old" )) {
						op |= OP_OLD;
					} else if (!strcmp( opt, "upgrade" )) {
						op |= OP_UPGRADE;
					} else if (!strcmp( opt, "renew" )) {
						renew_warn = 1;
						op |= OP_UPGRADE;
					} else if (!strcmp( opt, "gone" )) {
						op |= OP_GONE;
					} else if (!strcmp( opt, "delete" )) {
						delete_warn = 1;
						op |= OP_GONE;
					} else if (!strcmp( opt, "flags" )) {
						op |= OP_FLAGS;
					} else if (!strcmp( opt, "full" )) {
						op |= OP_MASK_TYPE;
					} else {
					  badopt:
						error( "Unknown option '%s'\n", argv[oind - 1] );
						return 1;
					}
					switch (op & XOP_MASK_DIR) {
					case XOP_PULL: mvars->ops[N] |= op & OP_MASK_TYPE; break;
					case XOP_PUSH: mvars->ops[F] |= op & OP_MASK_TYPE; break;
					default: cops |= op; break;
					}
					mvars->ops[F] |= XOP_HAVE_TYPE;
				}
				continue;
			}
			ochar = argv[oind++] + 1;
			if (!*ochar) {
				error( "Invalid option '-'\n" );
				return 1;
			}
		}
		switch (*ochar++) {
		case 'a':
			mvars->all = 1;
			break;
		case 'l':
			if (*ochar == 's')
				mvars->list_stores = 1, ochar++;
			else
				mvars->list = 1;
			break;
		case 'c':
			if (oind >= argc) {
				error( "-c requires an argument.\n" );
				return 1;
			}
			config = argv[oind++];
			break;
		case 'C':
			op = OP_CREATE | XOP_HAVE_CREATE;
		  cop:
			if (*ochar == 'f')
				mvars->ops[F] |= op, ochar++;
			else if (*ochar == 'm')  // Pre-1.4 legacy
				mvars->ops[F] |= op, ms_warn = 1, ochar++;
			else if (*ochar == 'n')
				mvars->ops[N] |= op, ochar++;
			else if (*ochar == 's')  // Pre-1.4 legacy
				mvars->ops[N] |= op, ms_warn = 1, ochar++;
			else if (*ochar == '-')
				ochar++;
			else
				cops |= op;
			mvars->ops[F] |= op & (XOP_HAVE_CREATE | XOP_HAVE_REMOVE | XOP_HAVE_EXPUNGE | XOP_HAVE_EXPUNGE_SOLO);
			break;
		case 'R':
			op = OP_REMOVE | XOP_HAVE_REMOVE;
			goto cop;
		case 'x':
			op = OP_EXPUNGE_SOLO | XOP_HAVE_EXPUNGE_SOLO;
			goto cop;
		case 'X':
			op = OP_EXPUNGE | XOP_HAVE_EXPUNGE;
			goto cop;
		case 'F':
			cops |= XOP_PULL | XOP_PUSH;
			mvars->ops[F] |= XOP_HAVE_TYPE;
			break;
		case '0':
			mvars->ops[F] |= XOP_TYPE_NOOP | XOP_HAVE_TYPE;
			break;
		case 'n':
		case 'o':
		case 'd':
		case 'g':
		case 'f':
		case 'N':
		case 'u':
			--ochar;
			op = 0;
		  cac:
			for (;; ochar++) {
				if (*ochar == 'n')
					op |= OP_NEW;
				else if (*ochar == 'o')
					op |= OP_OLD;
				else if (*ochar == 'g')
					op |= OP_GONE;
				else if (*ochar == 'd')
					op |= OP_GONE, delete_warn = 1;
				else if (*ochar == 'f')
					op |= OP_FLAGS;
				else if (*ochar == 'u')
					op |= OP_UPGRADE;
				else if (*ochar == 'N')
					op |= OP_UPGRADE, renew_warn = 1;
				else
					break;
			}
			if (op & OP_MASK_TYPE) {
				switch (op & XOP_MASK_DIR) {
				case XOP_PULL: mvars->ops[N] |= op & OP_MASK_TYPE; break;
				case XOP_PUSH: mvars->ops[F] |= op & OP_MASK_TYPE; break;
				default: cops |= op; break;
				}
			} else {
				cops |= op;
			}
			mvars->ops[F] |= XOP_HAVE_TYPE;
			break;
		case 'L':
			op = XOP_PULL;
			goto cac;
		case 'H':
			op = XOP_PUSH;
			goto cac;
		case 'q':
			if (Verbosity > VERYQUIET)
				Verbosity--;
			break;
		case 'V':
			Verbosity = VERBOSE;
			break;
		case 'D':
			for (op = 0; *ochar; ochar++) {
				switch (*ochar) {
				case 'C':
					op |= DEBUG_CRASH;
					break;
				case 'd':
					op |= DEBUG_DRV;
					break;
				case 'D':
					op |= DEBUG_DRV | DEBUG_DRV_ALL;
					break;
				case 'm':
					op |= DEBUG_MAILDIR;
					break;
				case 'M':
					op |= DEBUG_MAIN;
					break;
				case 'n':
					op |= DEBUG_NET;
					break;
				case 'N':
					op |= DEBUG_NET | DEBUG_NET_ALL;
					break;
				case 's':
					op |= DEBUG_SYNC;
					break;
				default:
					error( "Unknown -D flag '%c'\n", *ochar );
					return 1;
				}
			}
			if (!op)
				op = DEBUG_ALL;
			DFlags |= op;
			break;
		case 'y':
			DFlags |= DRYRUN;
			break;
		case 'e':
			DFlags |= EXT_EXIT;
			break;
		case 'T':
			for (; *ochar; ) {
				switch (*ochar++) {
				case 'a':
					DFlags |= FORCEASYNC(F);
					break;
				case 'A':
					DFlags |= FORCEASYNC(F) | FORCEASYNC(N);
					break;
				case 'j':
					DFlags |= KEEPJOURNAL;
					break;
				case 'J':
					DFlags |= FORCEJOURNAL;
					break;
				case 's':
					JLimit = strtol( ochar, &ochar, 10 );
					break;
				case 'u':
					DFlags |= FAKEDUMBSTORE;
					break;
				case 'x':
					DFlags |= FAKEEXPUNGE;
					break;
				case 'z':
					DFlags |= ZERODELAY;
					break;
				default:
					error( "Unknown -T flag '%c'\n", *(ochar - 1) );
					return 1;
				}
			}
			break;
		case 'v':
			version();
		case 'h':
			usage( 0 );
		default:
			error( "Unknown option '-%c'\n", *(ochar - 1) );
			return 1;
		}
	}
	if (ms_warn)
		warn( "Notice: -master/-slave/m/s suffixes are deprecated; use -far/-near/f/n instead.\n" );
	if (renew_warn)
		warn( "Notice: --renew/-N are deprecated; use --upgrade/-u instead.\n" );
	if (delete_warn)
		warn( "Notice: --delete/-d are deprecated; use --gone/-g instead.\n" );

	if (DFlags & DEBUG_ANY) {
		Verbosity = VERBOSE;

		fputs( PACKAGE " " VERSION " called with:", stdout );
		for (op = 1; op < argc; op++)
			printf( " '%s'", argv[op] );
		puts( "" );
	} else if (Verbosity >= TERSE && isatty( 1 )) {
		DFlags |= PROGRESS;
	}

#ifdef __linux__
	if (DFlags & DEBUG_CRASH) {
		signal( SIGSEGV, crashHandler );
		signal( SIGBUS, crashHandler );
		signal( SIGILL, crashHandler );
	}
#endif

	if (merge_ops( cops, mvars->ops, NULL ))
		return 1;

	if (load_config( config ))
		return 1;

	signal( SIGPIPE, SIG_IGN );

	if (pipe( child_pipe )) {
		perror( "pipe" );
		return 1;
	}
	fcntl( child_pipe[0], F_SETFL, O_NONBLOCK );
	fcntl( child_pipe[1], F_SETFL, O_NONBLOCK );
	init_notifier( &child_notifier, child_pipe[0], childReaper, NULL );
	conf_notifier( &child_notifier, 0, POLLIN );
	struct sigaction sa = { 0 };
	sa.sa_handler = childHandler;
	sa.sa_flags = SA_NOCLDSTOP | SA_RESTART;
	sigaction( SIGCHLD, &sa, NULL );

	if (mvars->list_stores)
		list_stores( mvars, argv + oind );
	else
		sync_chans( mvars, argv + oind );
	return mvars->ret;
}

void
cleanup_mainloop( void )
{
	cleanup_drivers();
	wipe_notifier( &child_notifier );
}
