diff --git a/src/.gitignore b/src/.gitignore
index 303b6bc..aa68809 100644
--- a/src/.gitignore
+++ b/src/.gitignore
@@ -2,3 +2,4 @@
.deps
subrelayd
subrelay-shell
+sbrlctrl
diff --git a/src/Makefile.am b/src/Makefile.am
index dff0637..85431c9 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -1,8 +1,8 @@
-bin_PROGRAMS = subrelayd subrelay-shell
+bin_PROGRAMS = subrelayd sbrlctrl
noinst_HEADERS = \
$(top_srcdir)/include/ilib/config.h \
- $(top_srcdir)/include/ilib/ipc.h \
+ $(top_srcdir)/include/ilib/local.h \
$(top_srcdir)/include/ilib/mail_engine.h \
$(top_srcdir)/include/ilib/mail_processor.h \
$(top_srcdir)/include/ilib/pending.h \
@@ -10,9 +10,7 @@ noinst_HEADERS = \
$(top_srcdir)/include/ilib/subscribers.h \
$(top_srcdir)/include/ilib/template.h
-# ------------------------------------------------------------------------------
-# subrelayd Daemon
-# ------------------------------------------------------------------------------
+# daemon sources
subrelayd_SOURCES = \
subrelayd.c \
splay.c \
@@ -22,18 +20,15 @@ subrelayd_SOURCES = \
template.c \
mail_engine.c \
mail_processor.c \
- ipc.c
+ local.c
-# Include internal header search path and PKG_CHECK libcurl flags
subrelayd_CPPFLAGS = -I$(top_srcdir)/include -D_POSIX_C_SOURCE=200809L -D_DEFAULT_SOURCE $(CURL_CFLAGS)
subrelayd_CFLAGS = -Wall -Wextra -pedantic -std=c99
subrelayd_LDADD = $(CURL_LIBS)
-# ------------------------------------------------------------------------------
-# subrelay-shell CLI Tool
-# ------------------------------------------------------------------------------
-subrelay_shell_SOURCES = \
- subrelay-shell.c
+# shell CLI for control daemon
+sbrlctrl_SOURCES = \
+ sbctrl.c
-subrelay_shell_CFLAGS = -Wall -Wextra -pedantic -std=c99
-subrelay_shell_CPPFLAGS = -I$(top_srcdir)/include -D_POSIX_C_SOURCE=200809L -D_DEFAULT_SOURCE
+sbrlctrl_CFLAGS = -Wall -Wextra -pedantic -std=c99
+sbrlctrl_CPPFLAGS = -I$(top_srcdir)/include -D_POSIX_C_SOURCE=200809L -D_DEFAULT_SOURCE
diff --git a/src/sbctrl.c b/src/sbctrl.c
new file mode 100644
index 0000000..379b550
--- /dev/null
+++ b/src/sbctrl.c
@@ -0,0 +1,143 @@
+/*
+ * this 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 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, see .";
+ *
+ * (c) Alexander Vdolainen 2026
+ * subrelay: a minimalistic maillist server based on external IMAPs/SMTPs
+ *
+ * sbctrl: maillist daemon control utility (main entry)
+ */
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define MAXCMD_LEN 1024
+#define READBUF_LEN 2048
+#define DEFAULT_SOCKET "/var/run/subrelay/subrelayd.sock"
+
+static void print_usage(const char *progname)
+{
+ printf("Subrelay control utility\n");
+ printf("Usage: %s -s [args...]\n\n", progname);
+ printf("Arguments:\n");
+ printf(" -s|--socket Path to subrelay local socket\n");
+ printf(" -h|--help Print help\n");
+ printf("Commands:\n");
+ printf(" ADD Directly subscribe an email address\n");
+ printf(" DEL Unsubscribe an email address\n");
+ printf(" LIST List all current subscribers\n");
+ printf(" STATUS Show daemon operational status\n");
+ return;
+}
+
+int main(int argc, char **argv)
+{
+ char *socket_path = DEFAULT_SOCKET;
+ char cmd_buf[MAXCMD_LEN];
+ size_t used = 0;
+ int opt;
+
+ while(1) {
+ int option_index = 0;
+ static struct option long_options[] = {
+ {"help", no_argument, NULL, 'h'},
+ {"socket", required_argument, NULL, 's'},
+ {NULL, 0, NULL, 0},
+ };
+
+ if((opt = getopt_long(argc, argv, "hs:", long_options, &option_index)) == -1)
+ break;
+
+ switch(opt) {
+ case 'h': print_usage(argv[0]); return 0; break;
+ case 's': socket_path = optarg; break;
+ default: print_usage(argv[0]); return EXIT_FAILURE; break;
+ }
+ }
+
+ if (optind >= argc) {
+ fprintf(stderr, "error: command required\n");
+ print_usage(argv[0]);
+ return EXIT_FAILURE;
+ }
+
+ /* Build command string from remaining arguments */
+ cmd_buf[0] = '\0';
+ for (int i = optind; i < argc; i++) {
+ size_t len = strlen(argv[i]);
+ size_t extra = len + (i < argc - 1 ? 1 : 0); /* argument + optional space */
+
+ /* Leave room for the final newline and the terminating nil */
+ if (extra > sizeof(cmd_buf) - used - 2) {
+ fprintf(stderr, "error: command or arguments too long.\n");
+ return EXIT_FAILURE;
+ }
+
+ memcpy(cmd_buf + used, argv[i], len);
+ used += len;
+
+ if (i < argc - 1) cmd_buf[used++] = ' ';
+ }
+
+ cmd_buf[used++] = '\n';
+ cmd_buf[used] = '\0';
+
+ /* Connect to subrelayd UNIX domain socket */
+ int fd = socket(AF_UNIX, SOCK_STREAM, 0);
+ if (fd < 0) {
+ perror("error opening socket");
+ return EXIT_FAILURE;
+ }
+
+ struct sockaddr_un addr;
+ memset(&addr, 0, sizeof(addr));
+ addr.sun_family = AF_UNIX;
+ if(strlen(socket_path) > sizeof(addr.sun_path)) {
+ fprintf(stderr, "Socket pathname is too long.\n");
+ close(fd);
+ return EXIT_FAILURE;
+ } else strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1);
+
+ if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
+ fprintf(stderr, "error: cannot connect to subrelayd at '%s': %s\n",
+ socket_path, strerror(errno));
+ close(fd);
+ return EXIT_FAILURE;
+ }
+
+ /* Send command */
+ if (write(fd, cmd_buf, strlen(cmd_buf)) < 0) {
+ perror("error writing command to socket");
+ close(fd);
+ return EXIT_FAILURE;
+ }
+
+ /* Read and print daemon response */
+ char resp[READBUF_LEN];
+ ssize_t bytes;
+
+ while ((bytes = read(fd, resp, sizeof resp)) > 0) {
+ if (fwrite(resp, 1, (size_t)bytes, stdout) != (size_t)bytes) break;
+ }
+
+ /* ignore read errors -- non critical */
+
+ close(fd);
+ return EXIT_SUCCESS;
+}
diff --git a/src/subrelay-shell.c b/src/subrelay-shell.c
deleted file mode 100644
index 085c5ce..0000000
--- a/src/subrelay-shell.c
+++ /dev/null
@@ -1,86 +0,0 @@
-/* src/subrelay-shell/main.c - Management CLI tool */
-#include
-#include
-#include
-#include
-#include
-#include
-#include
-
-static void print_usage(const char *progname)
-{
- printf("Subrelay Management Shell\n");
- printf("Usage: %s -s [args...]\n\n", progname);
- printf("Commands:\n");
- printf(" ADD Directly subscribe an email address\n");
- printf(" DEL Unsubscribe an email address\n");
- printf(" LIST List all current subscribers\n");
- printf(" STATUS Show daemon operational status\n");
-}
-
-int main(int argc, char **argv)
-{
- char *socket_path = "/var/run/subrelay/subrelayd.sock";
- int opt;
-
- while ((opt = getopt(argc, argv, "s:h")) != -1) {
- switch (opt) {
- case 's': socket_path = optarg; break;
- case 'h': print_usage(argv[0]); return EXIT_SUCCESS;
- default: print_usage(argv[0]); return EXIT_FAILURE;
- }
- }
-
- if (optind >= argc) {
- fprintf(stderr, "error: command required\n");
- print_usage(argv[0]);
- return EXIT_FAILURE;
- }
-
- /* Build command string from remaining arguments */
- char cmd_buf[1024];
- cmd_buf[0] = '\0';
-
- for (int i = optind; i < argc; i++) {
- strcat(cmd_buf, argv[i]);
- if (i < argc - 1) strcat(cmd_buf, " ");
- }
- strcat(cmd_buf, "\n");
-
- /* Connect to subrelayd UNIX domain socket */
- int fd = socket(AF_UNIX, SOCK_STREAM, 0);
- if (fd < 0) {
- perror("error opening socket");
- return EXIT_FAILURE;
- }
-
- struct sockaddr_un addr;
- memset(&addr, 0, sizeof(addr));
- addr.sun_family = AF_UNIX;
- strncpy(addr.sun_path, socket_path, sizeof(addr.sun_path) - 1);
-
- if (connect(fd, (struct sockaddr *)&addr, sizeof(addr)) != 0) {
- fprintf(stderr, "error: cannot connect to subrelayd at '%s': %s\n",
- socket_path, strerror(errno));
- close(fd);
- return EXIT_FAILURE;
- }
-
- /* Send command */
- if (write(fd, cmd_buf, strlen(cmd_buf)) < 0) {
- perror("error writing command to socket");
- close(fd);
- return EXIT_FAILURE;
- }
-
- /* Read and print daemon response */
- char resp[2048];
- ssize_t bytes;
- while ((bytes = read(fd, resp, sizeof(resp) - 1)) > 0) {
- resp[bytes] = '\0';
- printf("%s", resp);
- }
-
- close(fd);
- return EXIT_SUCCESS;
-}