dmc

dynamic mail client
git clone git://git.suckless.org/dmc
Log | Files | Refs | README | LICENSE

commit 319c99b3d1305c69ce8e0bb09815a068877d38da
parent fdeff1b6e7b09bedb1f87326fc70a90716503980
Author: pancake <nopcode.org>
Date:   Tue,  4 May 2010 01:56:45 +0200

* Mostly full replacement of dmc in C
  - old dmc is named dmc.sh (will be removed)
  - pop3 is fully working and mostly usable to
    be automatized for 'pull'
* dmc-pack pack mails when no arguments given)
Diffstat:
Makefile | 13+++++++++----
config.def.h | 4++++
dmc | 321-------------------------------------------------------------------------------
dmc-cmd.c | 272-------------------------------------------------------------------------------
dmc.c | 527+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
dmc.sh | 321+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
pack.c | 8+++++---
7 files changed, 866 insertions(+), 600 deletions(-)

diff --git a/Makefile b/Makefile @@ -7,13 +7,17 @@ endif CFLAGS+=-DHAVE_SSL=${HAVE_SSL} CFLAGS+=-DVERSION=\"${VERSION}\" +CFLAGS+=-DPREFIX=\"${PREFIX}\" -BINS=dmc-cmd dmc-mbox dmc-smtp dmc-pop3 dmc-imap4 dmc-pack dmc-filter dmc-mbox +BINS=dmc dmc-mbox dmc-smtp dmc-pop3 dmc-imap4 dmc-pack dmc-filter dmc-mbox -all: ${BINS} +all: config.h ${BINS} -dmc-cmd: dmc-cmd.o - ${CC} dmc-cmd.o -o dmc-cmd +config.h: + cp config.def.h config.h + +dmc: dmc.o + ${CC} dmc.o -o dmc dmc-smtp: smtp.o ${CC} ${LDFLAGS} smtp.o -o dmc-smtp -lresolv @@ -41,6 +45,7 @@ install: chmod +x dmc dmc-tag dmc-mdir cp -f dmc.1 ${PREFIX}/share/man/man1 cp -f dmc ${PREFIX}/bin + cp -f dmc.sh ${PREFIX}/bin cp -f dmc-tag ${PREFIX}/bin cp -f dmc-smtp ${PREFIX}/bin cp -f dmc-pop3 ${PREFIX}/bin diff --git a/config.def.h b/config.def.h @@ -0,0 +1,4 @@ +#define VERSION "0.1" +#define DMCDIR "/home/pancake/.dmc" +#define DIRPERM 0750 +#define EDITOR "vim" diff --git a/dmc b/dmc @@ -1,321 +0,0 @@ -#!/bin/sh -# dmc - dynamic mail client -# See LICENSE file for copyright and license details. - -VERSION="0.1" -HELP="Usage: dmc [-hv] [-e acc] [-a addr] [-m addr subj] [-A file] [-s msg] [action]" - -[ -z "${EDITOR}" ] && EDITOR=vim -mkdir -p ~/.dmc/mail -mkdir -p ~/.dmc/tmp -mkdir -p ~/.dmc/acc -[ -e ~/.dmc/acc.default ] && . ~/.dmc/acc.default - -chkcfg () { - if [ -z "${NAME}" ]; then - echo "No selected mail account. Use 'dmc -e'" - exit 1 - fi -} - -acc_daemon () { - chkcfg - LOCK=~/.dmc/tmp/${NAME}.lock - INPUT=~/.dmc/tmp/${NAME}.input - OUTPUT=~/.dmc/tmp/${NAME}.output - - echo "Starting $NAME account $PROTOCOL daemon..." - - rm -f "${LOCK}" "${INPUT}" "${OUTPUT}" - mkfifo "${LOCK}" "${INPUT}" - - # wait 1 connect message and 1 login message - (head -n 2 ${LOCK} ; dmc_cmd "login ${USER} ${PASS}" ; dmc_cmd "ls") & - (while : ; do cat ${INPUT} 2> /dev/null ; done) | \ - dmc-${PROTOCOL} ${HOST} ${PORT} ${SSL} 2> ${OUTPUT} > ${LOCK} - rm -f "${LOCK}" "${INPUT}" "${OUTPUT}" -} - -dmc_cmd () { - [ -z "$1" ] && return - echo "$@" > ~/.dmc/tmp/${NAME}.input - head -n 1 ~/.dmc/tmp/${NAME}.lock > /dev/stderr - cat ~/.dmc/tmp/${NAME}.output -} - -start_account_daemons () { - chkcfg - i=0 - for a in ~/.dmc/acc/* ; do - ( . $a ; acc_daemon ) & - i=$(($i+1)) - sleep 1 - done - if [ $i = 0 ]; then - echo "No accounts defined in ~/.dmc/acc" - exit 1 - fi -} - -print_account_template () { - echo "NAME='test'" - echo "SSL=0" - echo "LIMIT=50 # get only 50 mails for each folder" - echo "PROTOCOL='pop3' # imap4" - echo "HOST=serverhost.com" - echo "PORT=110" - echo "#SEND=acc-name" - echo "SEND=!msmtp" - echo "ONLINE=0" - echo "MAIL=username@domain" - echo "USER='username'" - echo "PASS='password'" -} - -edit_message () { - chkcfg - OUTDIR=~/.dmc/box/${NAME}/out - mkdir -p ${OUTDIR} - OUT="`mktemp ${OUTDIR}/mail.XXXXXX`" - echo "X-Mailer: dmc v${VERSION}" > $OUT - echo "From: ${MAIL}" >> $OUT - echo "To: ${TO}" >> $OUT - echo "Subject: ${SUBJECT}" >> $OUT - echo "" >> $OUT - echo "" >> $OUT - if [ -e ~/.dmc/box/${NAME}/signature ]; then - echo "---" >> $OUT - cat ~/.dmc/box/${NAME}/signature >> $OUT - else - if [ -e ~/.dmc/signature ]; then - echo "---" >> $OUT - cat ~/.dmc/signature >> $OUT - fi - fi - ${EDITOR} ${OUT} - if [ -z "`cat ${OUT}`" ]; then - echo "Mail aborted" - rm -f "${OUT}" - else - ln -fs "${OUT}" ~/.dmc/mail.last - fi -} - -add_attachment () { - chkcfg - OUT="`readlink ~/.dmc/mail.last`" - if [ -z "${OUT}" ]; then - echo "No ~/.dmc/mail.last found. 'dmc -m' or manual symlink required." - else - mkdir -p "${OUT}.d" - if [ -f "$1" ]; then - FILE="`basename \"$1\"`" - ln -fs "$1" "${OUT}.d/${FILE}" - else - echo "Cannot find \"$1\"" - fi - fi -} - -send_message () { - chkcfg - FILE=$1 - if [ ! -e "${FILE}" ]; then - echo "Cannot find '${FILE}'" - exit 1 - fi - # TODO: find better name for the auto mode - if [ -e "${FILE}.d" ]; then - TMP="`mktemp ~/.dmc/tmp/mail.XXXXXX`" - cat $FILE | dmc-pack `ls ${FILE}.d/*` > $TMP - else - TMP=$FILE - fi - if [ "${SEND}" = "!msmtp" ]; then - TO="`dmc-filter -v To: < $FILE`" - SJ="`dmc-filter -v Subject: < $FILE`" - echo "Sending mail to $TO (${SJ})..." -# HOST=`dmc-smtp ${TO}` - msmtp "--user=${USER}" "--from=${MAIL}" $TO < ${TMP} - return $? - elif [ "`echo \"${SEND}\" | grep '|'`" ]; then - echo "=> cat $1 ${SEND}" - # TODO: setup environment for $TO $SUBJECT ... - eval cat ${TMP} ${SEND} - else - echo "SEND method '${SEND}' not supported" - fi - [ -e "${FILE}.d" ] && rm -f $TMP - return 0 -} - -pull_mails () { - chkcfg - echo "Pulling mails from account '${NAME}'" - # This is pop3-only - i=1 - while [ ! "$LIMIT" = "$i" ] ; do - dmc -c cat $i > ~/.dmc/box/${NAME}/in/$i.eml 2> ~/.dmc/tmp/${NAME}.tmp - if [ -n "`cat ~/.dmc/tmp/${NAME}.tmp | grep 'cat 0'`" ]; then - rm -f ~/.dmc/box/${NAME}/in/$i.eml - echo "EOF $i" - cat ~/.dmc/tmp/${NAME}.tmp - break - else - size=`du -hs ~/.dmc/box/${NAME}/in/$i.eml | awk '{print \$1}'` - echo "got $i $size $(cat ~/.dmc/tmp/${NAME}.tmp)" - fi - i=$(($i+1)) - done -} - -ign () { : ; } - -case "$1" in -"start") - start_account_daemons - ;; -"stop") - cd ~/.dmc/tmp - for a in *.input ; do - [ "$a" = "*.input" ] && break # XXX ugly hack - file=`echo $a|cut -d '.' -f 1` - echo Stopping $file daemon - echo exit > $a & - sleep 1 - rm -f ~/.dmc/tmp/$a - done - rm -f ~/.dmc/tmp/* 2> /dev/null - pkill cat - trap ign TERM - pkill -TERM dmc - ;; -"push") - for a in ~/.dmc/acc/* ; do - . $a - [ ! -d ~/.dmc/box/${NAME}/out ] && continue - for b in ~/.dmc/box/${NAME}/out/* ; do - [ ! -f "$b" ] && continue - send_message "$b" - if [ $? = 0 ]; then - echo "Mail sent. local copy moved to ~/.dmc/box/${NAME}/sent" - mkdir -p ~/.dmc/box/${NAME}/sent - mv $b ~/.dmc/box/${NAME}/sent - [ -e "${b}.d" ] && mv ${b}.d ~/.dmc/box/${NAME}/sent - else - echo "There was an error sending the mail" - fi - done - done - ;; -"pull") - for a in ~/.dmc/acc/* ; do - . $a - mkdir -p ~/.dmc/box/${NAME}/in - pull_mails - done - ;; -"-e"|"--edit") - if [ -n "$2" ]; then - [ -z "`cat ~/.dmc/acc/$2`" ] && \ - print_account_template "$2" > ~/.dmc/acc/$2 - ${EDITOR} ~/.dmc/acc/$2 - if [ -z "`cat ~/.dmc/acc/$2`" ]; then - rm -f ~/.dmc/acc/$2 - else - echo "The '$2' account is now the default" - ln -fs ~/.dmc/acc/$2 ~/.dmc/acc.default - fi - else - ls ~/.dmc/acc | cat - fi - ;; -"-c"|"--cmd") - chkcfg - if [ -z "$2" ]; then - while [ ! "$A" = "exit" ] ; do - printf "> " - read A - dmc_cmd "$A" - done - else - shift - dmc_cmd "$@" - fi - ;; -"-s"|"--send") - shift - chkcfg - if [ -z "$*" ]; then - echo "Usage: dmc -s mail1 mail2 ..." - exit 1 - fi - for a in $* ; do - send_message $a - done - ;; -"-m"|"--mail") - TO="$2" - SUBJECT="$3" - edit_message - ;; -"-A"|"--add-attachment") - while [ -n "$2" ] ; do - add_attachment $2 - shift - done - ;; -"-a"|"--addr") - if [ -n "$2" ]; then - while [ -n "$2" ] ; do - grep -e "$2" ~/.dmc/addrbook - shift - done - else - [ ! -f ~/.dmc/addrbook ] && touch ~/.dmc/addrbook - ${EDITOR} ~/.dmc/addrbook - fi - ;; -"-l"|"--list") - cd ~/.dmc/box/${NAME}/in - if [ -n "$2" ]; then - if [ -f "$2.eml" ]; then - cat $2.eml - exit 0 - fi - cd $2 - fi - for a in `ls -rt *.eml` ; do - printf "$a:\t\x1b[32m" - dmc-filter Subject < $a | head -n 1 - printf "\x1b[0m \t- " - dmc-filter To < $a | head -n 1 - printf " \t- " - dmc-filter Date < $a | head -n 1 - done - ;; -"-v"|"--version") - echo "dmc v${VERSION}" - ;; -"--help"|"-h") - echo ${HELP} - echo " -e account edit account information" - echo " -a name show addressbook email for contact" - echo " -A file add attachment to mail" - echo " -c cmd run command for \$DMC_ACCOUNT or acc.default daemon" - echo " -m addr subj create mail with default account" - echo " -s file send email" - echo " -l [mail/fold] list mails in folder or show mail" - echo " -v show version" - echo " -h show this help message" - echo " start start mail daemons" - echo " stop stop them all" - echo " push send mails in box/*/out" - echo " pull update box/*/in folders" - ;; -*) - echo "${HELP}" - ;; -esac - -exit 0 diff --git a/dmc-cmd.c b/dmc-cmd.c @@ -1,272 +0,0 @@ -#include <stdio.h> -#include <string.h> -#include <dirent.h> -#include <unistd.h> -#include <stdlib.h> -#include <sys/stat.h> - -#define DMCDIR "/home/pancake/.dmc" -#define EDITOR "vim" -#define VERSION "0.1" - -static char prompt[64]; -static char **acc; - -static int dmcstart(const char *name) { - return 0; -} - -static int dmcstop(const char *name) { - return 0; -} - -static char *dmccmd(const char *cmd, char *err, int errlen) { - return 0; -} - -static int dmcpull(const char *name) { - return 0; -} - -static int dmcpush(const char *name) { - return 0; -} - -#define MAXACC 4 -static char **dmcaccounts() { - struct dirent *de; - static char *accounts[MAXACC]; - char buf[256], *defacc = NULL; - DIR *dir; - int acc = 0; - memset (buf, 0, sizeof (buf)); - if (readlink (DMCDIR"/acc.default", buf, sizeof (buf))!=-1) { - defacc = buf + strlen (buf)-1; - while (*defacc != '/') - defacc--; - accounts[acc++] = strdup (++defacc); - } else fprintf (stderr, "No default account defined.\n"); - dir = opendir (DMCDIR"/acc"); - while ((de = readdir (dir))) - if (*de->d_name != '.' && (!defacc || strcmp (de->d_name, defacc))) - accounts[acc++] = strdup (de->d_name); - accounts[acc] = NULL; - return accounts; -} - -static void charrfree(char **ptr) { - char **p = ptr; - while (*p) { - free(*p); - p++; - } - *ptr = NULL; -} - -static int dmcline(const char *line) { - char err[128]; - char *ptr = strchr (line, ' '); - if (ptr) - *ptr = '\0'; - if (!strcmp (line, "?")) { - printf ("Usage: push pull exit\n"); - } else - if (!strcmp (line, "push")) { - dmcpush (acc[0]); - } else - if (!strcmp (line, "pull")) { - dmcstart (acc[0]); - dmcpull (acc[0]); - dmcstop (acc[0]); - } else - if (!strcmp (line, "exit")) { - return 0; - } else { - /* bypass to child */ - ptr = dmccmd (line, err, sizeof (err)); - if (ptr) { - printf ("%s\n", ptr); - fprintf (stderr, "%s\n", err); - free (ptr); - } else fprintf (stderr, "## No reply\n"); - } - return 1; -} - -static int usage(const char *argv0, int lon) { - fprintf (stderr, "Usage: %s [-hv] [-s file] [-e acc] [-a addr] [-m [A|S A]] [-l [box]]\n", argv0); - if (lon) { - fprintf (stderr, - " -m [A|S A .. ] ; create mail (Subject, Address, ..)\n" - " -c [cmd] ; command shell\n" - " -e [acc] ; list/edit/set default accounts\n" - " -a [addr] ; grep in addressbook\n" - " -l [box] ; list mails in specified box of def account\n" - " -s [file] ; send mail\n" - " -v ; show version\n" - ""); - } - return 0; -} - -static int fexist(const char *path) { - struct stat st; - int ret = stat (path, &st); - return (ret != -1); -} - -static int fsize(const char *path) { - FILE *fd = fopen (path, "r"); - int len = 0; - if (fd) { - len = fseek (fd, 0, SEEK_END); - fclose (fd); - } - return len; -} - -int main(int argc, char **argv) { - char file[128], line[128]; - struct dirent *de; - DIR *dir; - int i; - - if (argc < 2) - return usage (argv[0], 0); - - acc = dmcaccounts (); - - if (argv[1][0] == '-') - switch (argv[1][1]) { - case 'l': - if (argc>2) { - snprintf (line, sizeof (line), DMCDIR"/box/%s/in/%s.eml", acc[0], argv[2]); - if (fexist (line)) { - snprintf (line, sizeof (line), "cat '"DMCDIR"/box/%s/in/%s.eml'", - acc[0], argv[2]); - system (line); // implement native cat - break; - } - snprintf (line, sizeof (line), DMCDIR"/box/%s", argv[2]); - } else snprintf (line, sizeof (line), DMCDIR"/box/%s/in", acc[0]); - charrfree (acc); - if ((dir = opendir (line))) { - while ((de = readdir (dir))) { - if (*de->d_name!='.') - printf ("%s:\n", de->d_name); - } - closedir (dir); - } - break; - case 'v': - printf ("dmc v"VERSION"\n"); - break; - case 's': - case 'm': - fprintf (stderr, "TODO\n"); - break; - case 'e': - if (argc<3) { - for (i=0; acc[i]; i++) - printf ("%s\n", acc[i]); - } else { - snprintf (file, sizeof (file), DMCDIR"/acc/%s", argv[2]); - if (!fexist (file)) { - /* create template */ - FILE *fd = fopen (file, "w"); - if (fd) { - fprintf (fd, - "NAME=test\n" - "SSL=0\n" - "LIMIT=50 # get only 50 mails for each folder\n" - "PROTOCOL=pop3 # imap4\n" - "HOST=serverhost.com\n" - "PORT=110\n" - "#SEND=acc-name\n" - "SEND=!msmtp\n" - "MAIL=username@domain.com\n" - "USER=username\n" - "PASS=password\n"); - fclose (fd); - } else { - fprintf (stderr, "Cannot write in %s\n", file); - return 1; - } - } - snprintf (line, sizeof (line), EDITOR" %s", file); - system (line); - if (fsize(file)<32) { - printf ("Abort\n"); - unlink (file); - } else { - printf ("Default account has changed.\n"); - unlink (DMCDIR"/acc.default"); - symlink (file, DMCDIR"/acc.default"); - } - } - charrfree (acc); - return 0; - case 'a': - if (argc<3) { - snprintf (line, sizeof (line), EDITOR" '"DMCDIR"/addrbook'"); - system (line); - } else { - FILE *fd = fopen (DMCDIR"/addrbook", "r"); - if (fd) { - while (!feof (fd)) { - fgets (line, sizeof (line), fd); - for (i=2; i<argc; i++) { - if (strstr (line, argv[i])) - printf ("%s", line); - } - } - fclose (fd); - } - } - break; - case 'c': - strcpy (prompt, "dmc> "); - do { - write (1, prompt, strlen (prompt)); - fgets(line, sizeof (line), stdin); - if (feof (stdin)) - strcpy (line, "exit"); - else line[strlen (line)-1] = '\0'; - } while (dmcline (line)); - break; - default: - return usage (argv[0], 1); - } - - charrfree (acc); - return 0; -} - -#if 0 - $ echo pull | dmc-cmd - $ dmc-cmd pull - $ dmc - dmc> ls - nopcode - radare - dmc> use nopcode - # start dmc-pop3 connection - dmc/nopcode> login [user/pass] - # +OK (user/pass) are optional - dmc/nopcode> ls - -Commands -======== -Handled by dmc-cmd: - ? : alias for help - use <accountname> - pull - push - help - -Handled by dmc-pop3|imap4|...: - exit: handled by child - cd <change directory> - help - -#endif diff --git a/dmc.c b/dmc.c @@ -0,0 +1,527 @@ +/* dmc - dynamic mail client + * See LICENSE file for copyright and license details. + */ +#include <stdio.h> +#include <string.h> +#include <dirent.h> +#include <unistd.h> +#include <stdlib.h> +#include <sys/stat.h> +#include <sys/wait.h> +#include <sys/types.h> +#include "config.h" + +static char **dmcaccounts(); +static char prompt[64]; +static char **acc, *defacc = NULL; +static int dmc_in[2], dmc_out[2], dmc_err[2]; +static int dmc_pid = -1; +static fd_set rfds, wfds; + +static void dmcstop(); +static int dmcsend(const char *file); +static int fexist(const char *path); +extern int errno; + +static void dmcinit() { + if (!fexist (DMCDIR)) + if (mkdir (DMCDIR, DIRPERM) == -1) { + fprintf (stderr, "Cannot create "DMCDIR"\n"); + exit (1); + } + mkdir (DMCDIR"/tmp", DIRPERM); + mkdir (DMCDIR"/box", DIRPERM); + mkdir (DMCDIR"/acc", DIRPERM); + acc = dmcaccounts (); + defacc = acc[0]; + signal (SIGINT, dmcstop); + atexit (dmcstop); +} + +static char *cfgget(const char *key) { + FILE *fd; + char file[128], line[128], *ptr, *ret = NULL; + snprintf (file, sizeof (file), DMCDIR"/acc/%s", defacc); + if ((fd = fopen (file, "r"))) { + int len = strlen (key); + while (!feof (fd)) { + fgets (line, sizeof (line), fd); + if (!memcmp (line, key, len)) { + ptr = strchr (line, '#'); + if (ptr) + for (*ptr=0, ptr--; ptr != line && (*ptr==' ' || *ptr=='\t'); ptr--) + *ptr = 0; + line [strlen (line)-1] = 0; + ret = strdup (line+len); + break; + } + } + fclose (fd); + } + return ret; +} + +static char *gethdr(const char *path, const char *file, const char *hdr) { + char line[1024], *ret = NULL; + FILE *fd; + snprintf (line, sizeof (line), "%s/%s", path, file); + fd = fopen (line, "r"); + if (fd) { + int len = strlen (hdr); + while (!feof (fd)) { + fgets (line, sizeof (line), fd); + if (!memcmp (line, hdr, len)) { + line[strlen (line)-2] = '\0'; + ret = strdup (line+len); + break; + } + } + fclose (fd); + } + return ret; +} + +/* server */ +static int dmcstart(const char *name) { + char *host, *port; + pipe (dmc_in); + pipe (dmc_out); + pipe (dmc_err); + dmc_pid = fork (); + signal (SIGPIPE, SIG_IGN); + switch (dmc_pid) { + case -1: + fprintf (stderr, "Cannot fork\n"); + exit (1); + case 0: + dup2 (dmc_in[0], 0); + close (dmc_in[0]); + close (dmc_in[1]); + + dup2 (dmc_out[1], 1); + close (dmc_out[0]); + close (dmc_out[1]); + + dup2 (dmc_err[1], 2); + close (dmc_err[0]); + close (dmc_err[1]); + + host = cfgget ("HOST="); + port = cfgget ("PORT="); + execl (PREFIX"/bin/dmc-pop3", "dmc-pop3", host, port, NULL); + free (host); + free (port); + exit (1); + default: + close (dmc_out[1]); + close (dmc_err[1]); + close (dmc_in[0]); + break; + } + return 0; +} + +static void dmckill(int sig) { + fprintf (stderr, "Killed.\n"); + kill (dmc_pid, SIGKILL); +} + +static void dmcstop() { + if (dmc_pid != -1) { + const char *cmd = "exit\n"; + write (dmc_in[1], cmd, strlen (cmd)); + signal (SIGALRM, dmckill); + alarm (2); + waitpid (dmc_pid, NULL, 0); + signal (SIGALRM, NULL); + alarm (0); + dmc_pid = -1; + + // duppy + close (dmc_in[0]); + close (dmc_in[1]); + close (dmc_out[0]); + close (dmc_out[1]); + close (dmc_err[0]); + close (dmc_err[1]); + } +} + +static char *dmccmd(const char *cmd, char *err, int errlen) { + static char buf[4096]; + int ret, nfd; + + if (dmc_pid == -1) { + printf ("Use 'start' or '?'\n"); + return NULL; + } + for (;;) { + FD_ZERO (&rfds); + FD_ZERO (&wfds); + FD_SET (dmc_out[0], &rfds); + FD_SET (dmc_err[0], &rfds); + FD_SET (dmc_in[1], &wfds); + nfd = select (dmc_err[0] + 1, &rfds, &wfds, NULL, NULL); + if (nfd < 0) + break; + if (FD_ISSET (dmc_out[0], &rfds)) { + ret = read (dmc_out[0], buf, sizeof (buf)-1); + if (ret>0) { + buf[ret-1] = 0; + printf ("-(out)-> (%s)\n", buf); + break; + } + } else + if (FD_ISSET (dmc_err[0], &rfds)) { + ret = read (dmc_err[0], buf, sizeof (buf)-1); + if (ret>0) { + buf[ret-1] = 0; + printf ("-(err)-> (%s)\n", buf); + } + } else + if (FD_ISSET (dmc_in[1], &wfds)) { + if (cmd && *cmd && *cmd != '\n') { + snprintf (buf, sizeof (buf), "%s\n", cmd); + write (dmc_in[1], buf, strlen (buf)); + printf ("write (%s)\n", cmd); + cmd = NULL; + } + } + } + return buf; +} + +static int dmcpull(const char *name) { + return 0; +} + +static void dmcpush(const char *name) { + struct dirent *de; + char path[256], file[512]; + DIR *dir; + int i; + + for (i=0; acc[i]; i++) { + snprintf (path, sizeof (path), DMCDIR"/box/%s/out", acc[i]); + dir = opendir (path); + while ((de = readdir (dir))) + if (*de->d_name != '.') { + snprintf (file, sizeof (file), "%s/%s", path, de->d_name); + printf ("--> %s\n", file); + dmcsend (file); + snprintf (file, sizeof (file), "rm -rf %s*", file); + printf ("--> (%s)\n", file); + //system (file); + } + closedir (dir); + } +} + +/* utils */ +static int fexist(const char *path) { + struct stat st; + int ret = stat (path, &st); + return (ret != -1); +} + +static int fsize(const char *path) { + FILE *fd = fopen (path, "r"); + int len = 0; + if (fd) { + fseek (fd, 0, SEEK_END); + len = ftell (fd); + fclose (fd); + } + return len; +} + +#define MAXACC 4 +static char **dmcaccounts() { + struct dirent *de; + static char *accounts[MAXACC]; + char buf[256], *defacc = NULL; + DIR *dir; + int acc = 0; + memset (buf, 0, sizeof (buf)); + if (readlink (DMCDIR"/acc.default", buf, sizeof (buf))!=-1) { + defacc = buf + strlen (buf)-1; + while (*defacc != '/') + defacc--; + accounts[acc++] = strdup (++defacc); + } else fprintf (stderr, "No default account defined.\n"); + dir = opendir (DMCDIR"/acc"); + while ((de = readdir (dir)) && acc<MAXACC-2) + if (de->d_type == DT_REG) + if (*de->d_name != '.' && (!defacc || strcmp (de->d_name, defacc))) + accounts[acc++] = strdup (de->d_name); + accounts[acc] = NULL; + return accounts; +} + +static void charrfree(char **ptr) { + char **p = ptr; + while (*p) { + free(*p); + p++; + } + *ptr = NULL; +} + +static int dmcsend(const char *file) { + char line[512]; + char *user = cfgget ("USER="); + char *mail = cfgget ("MAIL="); + char *to = gethdr ("/", file, "To: "); + /* TODO: Implement non-msmtp method */ + snprintf (line, sizeof (line), + "dmc-pack `ls %s.d/* 2>/dev/null` < %s " + "| msmtp --user=\"%s\" --from=\"%s\" \"%s\" < %s", + file, file, + user, mail, to, file); + if (system (line) != 0) { + fprintf (stderr, "Error ocurred while sending %s\n", file); + return 0; + } else fprintf (stderr, "Sent.\n"); + return 1; +} + +static int dmcline(const char *line) { + char err[128]; + char *ptr = strchr (line, ' '); + if (ptr) + *ptr = '\0'; + if (!strcmp (line, "?")) { + printf ("Usage: start stop push pull exit ls cat ..\n"); + } else + if (!strcmp (line, "start")) { + dmcstart (acc[0]); + ptr = dmccmd ("start", err, sizeof (err)); + } else + if (!strcmp (line, "stop")) { + dmcstop (); + } else + if (!strcmp (line, "push")) { + dmcpush (acc[0]); + } else + if (!strcmp (line, "pull")) { + if (dmcstart (acc[0])) { + dmcpull (acc[0]); + dmcstop (); + } + } else + if (!strcmp (line, "exit")) { + return 0; + } else { + if (ptr) + *ptr = ' '; + /* bypass to child */ + ptr = dmccmd (line, err, sizeof (err)); + if (ptr) { + printf ("%s\n", ptr); + fprintf (stderr, "%s\n", err); + //free (ptr); + } else fprintf (stderr, "## No reply\n"); + } + return 1; +} + +static int usage(const char *argv0, int lon) { + fprintf (stderr, "Usage: %s [-hv] [-c [cmd] [-s file] [-e acc] [-A file ..]\n" + "\t [-a addr] [-m [A|S A]] [-l [box]]\n", argv0); + if (lon) { + fprintf (stderr, + " -m [A [S]] create mail (Address, Subject)\n" + " -c [cmd] command shell\n" + " -e [acc] list/edit/set default accounts\n" + " -a [addr] grep in addressbook\n" + " -A [file .] attach files to ~/.dmc/mail.last done by 'dmc -m'\n" + " -l [box] list mails in specified box of def account\n" + " -s [file] send mail\n" + //" -f [file] forward mail\n" + //" -r [file] reply mail\n" + " -v show version\n" + ""); + } + return 0; +} + +static int dmcmail(const char *addr, const char *subj) { + const char *from = "todo@from.com"; + char file[128], line[128]; + int fd; + snprintf (file, sizeof (file), DMCDIR"/box/%s/out/mail.XXXXXX", acc[0]); + fd = mkstemp (file); + if (fd != -1) { + snprintf (line, sizeof (line), + "X-Mailer: dmc v"VERSION"\n" + "From: %s\n" + "To: %s\n" + "Subject: %s\n\n\n", from, addr, subj); + write (fd, line, strlen (line)); + close (fd); + snprintf (line, sizeof (line), EDITOR" %s", file); + system (line); + if (fsize (file)<32) { + fprintf (stderr, "Aborted\n"); + unlink (file); + } else symlink (file, DMCDIR"/mail.last"); + } else fprintf (stderr, "Cannot create '%s'\n", line); + return 0; +} + +static void dmcattach(const char *file) { + char path[256]; + const char *name = file + strlen (file)-1; + while (*name!='/' && name>file) + name--; + name++; + if (readlink (DMCDIR"/mail.last", path, sizeof (path)-10)!=-1) { + strcat (path, ".d/"); + mkdir (path, 0750); + strcat (path, name); + symlink (file, path); + } else fprintf (stderr, "Cannot attach '%s'\n", file); +} + +int main(int argc, char **argv) { + char file[128], line[128]; + struct dirent *de; + DIR *dir; + int i; + + if (argc < 2) + return usage (argv[0], 0); + + dmcinit (); + if (strcmp (argv[1], "-e")) { + if (!acc[0]) { + fprintf (stderr, "Use 'dmc -e' to configure the account\n"); + return 1; + } + } + + if (argv[1][0] == '-') + switch (argv[1][1]) { + case 'l': + if (argc>2) { + snprintf (line, sizeof (line), DMCDIR"/box/%s/in/%s.eml", acc[0], argv[2]); + if (fexist (line)) { + snprintf (line, sizeof (line), "cat '"DMCDIR"/box/%s/in/%s.eml'", + acc[0], argv[2]); + system (line); // implement native cat + break; + } + snprintf (line, sizeof (line), DMCDIR"/box/%s", argv[2]); + } else snprintf (line, sizeof (line), DMCDIR"/box/%s/in", acc[0]); + if ((dir = opendir (line))) { + while ((de = readdir (dir))) { + if (*de->d_name!='.') { + char *subj = gethdr (line, de->d_name, "Subject: "); + char *from = gethdr (line, de->d_name, "From: "); + printf ("%s:\t%s\n\t%s\n", de->d_name, subj, from); + free (from); + free (subj); + } + } + closedir (dir); + } + break; + case 'A': + for (i=2; i<argc; i++) + dmcattach (argv[i]); + break; + case 'v': + printf ("dmc v"VERSION"\n"); + break; + case 's': + if (argc>2) + dmcsend (argv[2]); + else fprintf (stderr, "Usage: dmc-cmd [-s file]\n"); + break; + case 'm': + { + char *addr = ""; + char *subj = ""; + if (argc>2) + addr = argv[2]; + if (argc>3) + subj = argv[3]; + dmcmail (addr, subj); + } + break; + case 'e': + if (argc<3) { + for (i=0; acc[i]; i++) + printf ("%s\n", acc[i]); + } else { + snprintf (file, sizeof (file), DMCDIR"/acc/%s", argv[2]); + if (!fexist (file)) { + /* create template */ + FILE *fd = fopen (file, "w"); + if (fd) { + fprintf (fd, + "NAME=test\n" + "SSL=0\n" + "LIMIT=50 # get only 50 mails for each folder\n" + "PROTOCOL=pop3 # imap4\n" + "HOST=serverhost.com\n" + "PORT=110\n" + "#SEND=acc-name\n" + "SEND=!msmtp\n" + "MAIL=username@domain.com\n" + "USER=username\n" + "PASS=password\n"); + fclose (fd); + } else { + fprintf (stderr, "Cannot write in %s\n", file); + return 1; + } + } + snprintf (line, sizeof (line), EDITOR" %s", file); + system (line); + if (fsize(file)<32) { + printf ("Abort\n"); + unlink (file); + } else { + printf ("Default account has changed.\n"); + unlink (DMCDIR"/acc.default"); + symlink (file, DMCDIR"/acc.default"); + } + } + break; + case 'a': + if (argc<3) { + snprintf (line, sizeof (line), EDITOR" '"DMCDIR"/addrbook'"); + system (line); + } else { + FILE *fd = fopen (DMCDIR"/addrbook", "r"); + if (fd) { + while (!feof (fd)) { + fgets (line, sizeof (line), fd); + for (i=2; i<argc; i++) { + if (strstr (line, argv[i])) + printf ("%s", line); + } + } + fclose (fd); + } + } + break; + case 'c': + strcpy (prompt, "dmc> "); + do { + write (1, prompt, strlen (prompt)); + fgets(line, sizeof (line), stdin); + if (feof (stdin)) + strcpy (line, "exit"); + else line[strlen (line)-1] = '\0'; + } while (dmcline (line)); + dmcstop (); + break; + default: + return usage (argv[0], 1); + } + + charrfree (acc); + return 0; +} diff --git a/dmc.sh b/dmc.sh @@ -0,0 +1,321 @@ +#!/bin/sh +# dmc - dynamic mail client +# See LICENSE file for copyright and license details. + +VERSION="0.1" +HELP="Usage: dmc [-hv] [-e acc] [-a addr] [-m addr subj] [-A file] [-s msg] [action]" + +[ -z "${EDITOR}" ] && EDITOR=vim +mkdir -p ~/.dmc/mail +mkdir -p ~/.dmc/tmp +mkdir -p ~/.dmc/acc +[ -e ~/.dmc/acc.default ] && . ~/.dmc/acc.default + +chkcfg () { + if [ -z "${NAME}" ]; then + echo "No selected mail account. Use 'dmc -e'" + exit 1 + fi +} + +acc_daemon () { + chkcfg + LOCK=~/.dmc/tmp/${NAME}.lock + INPUT=~/.dmc/tmp/${NAME}.input + OUTPUT=~/.dmc/tmp/${NAME}.output + + echo "Starting $NAME account $PROTOCOL daemon..." + + rm -f "${LOCK}" "${INPUT}" "${OUTPUT}" + mkfifo "${LOCK}" "${INPUT}" + + # wait 1 connect message and 1 login message + (head -n 2 ${LOCK} ; dmc_cmd "login ${USER} ${PASS}" ; dmc_cmd "ls") & + (while : ; do cat ${INPUT} 2> /dev/null ; done) | \ + dmc-${PROTOCOL} ${HOST} ${PORT} ${SSL} 2> ${OUTPUT} > ${LOCK} + rm -f "${LOCK}" "${INPUT}" "${OUTPUT}" +} + +dmc_cmd () { + [ -z "$1" ] && return + echo "$@" > ~/.dmc/tmp/${NAME}.input + head -n 1 ~/.dmc/tmp/${NAME}.lock > /dev/stderr + cat ~/.dmc/tmp/${NAME}.output +} + +start_account_daemons () { + chkcfg + i=0 + for a in ~/.dmc/acc/* ; do + ( . $a ; acc_daemon ) & + i=$(($i+1)) + sleep 1 + done + if [ $i = 0 ]; then + echo "No accounts defined in ~/.dmc/acc" + exit 1 + fi +} + +print_account_template () { + echo "NAME='test'" + echo "SSL=0" + echo "LIMIT=50 # get only 50 mails for each folder" + echo "PROTOCOL='pop3' # imap4" + echo "HOST=serverhost.com" + echo "PORT=110" + echo "#SEND=acc-name" + echo "SEND=!msmtp" + echo "ONLINE=0" + echo "MAIL=username@domain" + echo "USER='username'" + echo "PASS='password'" +} + +edit_message () { + chkcfg + OUTDIR=~/.dmc/box/${NAME}/out + mkdir -p ${OUTDIR} + OUT="`mktemp ${OUTDIR}/mail.XXXXXX`" + echo "X-Mailer: dmc v${VERSION}" > $OUT + echo "From: ${MAIL}" >> $OUT + echo "To: ${TO}" >> $OUT + echo "Subject: ${SUBJECT}" >> $OUT + echo "" >> $OUT + echo "" >> $OUT + if [ -e ~/.dmc/box/${NAME}/signature ]; then + echo "---" >> $OUT + cat ~/.dmc/box/${NAME}/signature >> $OUT + else + if [ -e ~/.dmc/signature ]; then + echo "---" >> $OUT + cat ~/.dmc/signature >> $OUT + fi + fi + ${EDITOR} ${OUT} + if [ -z "`cat ${OUT}`" ]; then + echo "Mail aborted" + rm -f "${OUT}" + else + ln -fs "${OUT}" ~/.dmc/mail.last + fi +} + +add_attachment () { + chkcfg + OUT="`readlink ~/.dmc/mail.last`" + if [ -z "${OUT}" ]; then + echo "No ~/.dmc/mail.last found. 'dmc -m' or manual symlink required." + else + mkdir -p "${OUT}.d" + if [ -f "$1" ]; then + FILE="`basename \"$1\"`" + ln -fs "$1" "${OUT}.d/${FILE}" + else + echo "Cannot find \"$1\"" + fi + fi +} + +send_message () { + chkcfg + FILE=$1 + if [ ! -e "${FILE}" ]; then + echo "Cannot find '${FILE}'" + exit 1 + fi + # TODO: find better name for the auto mode + if [ -e "${FILE}.d" ]; then + TMP="`mktemp ~/.dmc/tmp/mail.XXXXXX`" + cat $FILE | dmc-pack `ls ${FILE}.d/*` > $TMP + else + TMP=$FILE + fi + if [ "${SEND}" = "!msmtp" ]; then + TO="`dmc-filter -v To: < $FILE`" + SJ="`dmc-filter -v Subject: < $FILE`" + echo "Sending mail to $TO (${SJ})..." +# HOST=`dmc-smtp ${TO}` + msmtp "--user=${USER}" "--from=${MAIL}" $TO < ${TMP} + return $? + elif [ "`echo \"${SEND}\" | grep '|'`" ]; then + echo "=> cat $1 ${SEND}" + # TODO: setup environment for $TO $SUBJECT ... + eval cat ${TMP} ${SEND} + else + echo "SEND method '${SEND}' not supported" + fi + [ -e "${FILE}.d" ] && rm -f $TMP + return 0 +} + +pull_mails () { + chkcfg + echo "Pulling mails from account '${NAME}'" + # This is pop3-only + i=1 + while [ ! "$LIMIT" = "$i" ] ; do + dmc -c cat $i > ~/.dmc/box/${NAME}/in/$i.eml 2> ~/.dmc/tmp/${NAME}.tmp + if [ -n "`cat ~/.dmc/tmp/${NAME}.tmp | grep 'cat 0'`" ]; then + rm -f ~/.dmc/box/${NAME}/in/$i.eml + echo "EOF $i" + cat ~/.dmc/tmp/${NAME}.tmp + break + else + size=`du -hs ~/.dmc/box/${NAME}/in/$i.eml | awk '{print \$1}'` + echo "got $i $size $(cat ~/.dmc/tmp/${NAME}.tmp)" + fi + i=$(($i+1)) + done +} + +ign () { : ; } + +case "$1" in +"start") + start_account_daemons + ;; +"stop") + cd ~/.dmc/tmp + for a in *.input ; do + [ "$a" = "*.input" ] && break # XXX ugly hack + file=`echo $a|cut -d '.' -f 1` + echo Stopping $file daemon + echo exit > $a & + sleep 1 + rm -f ~/.dmc/tmp/$a + done + rm -f ~/.dmc/tmp/* 2> /dev/null + pkill cat + trap ign TERM + pkill -TERM dmc + ;; +"push") + for a in ~/.dmc/acc/* ; do + . $a + [ ! -d ~/.dmc/box/${NAME}/out ] && continue + for b in ~/.dmc/box/${NAME}/out/* ; do + [ ! -f "$b" ] && continue + send_message "$b" + if [ $? = 0 ]; then + echo "Mail sent. local copy moved to ~/.dmc/box/${NAME}/sent" + mkdir -p ~/.dmc/box/${NAME}/sent + mv $b ~/.dmc/box/${NAME}/sent + [ -e "${b}.d" ] && mv ${b}.d ~/.dmc/box/${NAME}/sent + else + echo "There was an error sending the mail" + fi + done + done + ;; +"pull") + for a in ~/.dmc/acc/* ; do + . $a + mkdir -p ~/.dmc/box/${NAME}/in + pull_mails + done + ;; +"-e"|"--edit") + if [ -n "$2" ]; then + [ -z "`cat ~/.dmc/acc/$2`" ] && \ + print_account_template "$2" > ~/.dmc/acc/$2 + ${EDITOR} ~/.dmc/acc/$2 + if [ -z "`cat ~/.dmc/acc/$2`" ]; then + rm -f ~/.dmc/acc/$2 + else + echo "The '$2' account is now the default" + ln -fs ~/.dmc/acc/$2 ~/.dmc/acc.default + fi + else + ls ~/.dmc/acc | cat + fi + ;; +"-c"|"--cmd") + chkcfg + if [ -z "$2" ]; then + while [ ! "$A" = "exit" ] ; do + printf "> " + read A + dmc_cmd "$A" + done + else + shift + dmc_cmd "$@" + fi + ;; +"-s"|"--send") + shift + chkcfg + if [ -z "$*" ]; then + echo "Usage: dmc -s mail1 mail2 ..." + exit 1 + fi + for a in $* ; do + send_message $a + done + ;; +"-m"|"--mail") + TO="$2" + SUBJECT="$3" + edit_message + ;; +"-A"|"--add-attachment") + while [ -n "$2" ] ; do + add_attachment $2 + shift + done + ;; +"-a"|"--addr") + if [ -n "$2" ]; then + while [ -n "$2" ] ; do + grep -e "$2" ~/.dmc/addrbook + shift + done + else + [ ! -f ~/.dmc/addrbook ] && touch ~/.dmc/addrbook + ${EDITOR} ~/.dmc/addrbook + fi + ;; +"-l"|"--list") + cd ~/.dmc/box/${NAME}/in + if [ -n "$2" ]; then + if [ -f "$2.eml" ]; then + cat $2.eml + exit 0 + fi + cd $2 + fi + for a in `ls -rt *.eml` ; do + printf "$a:\t\x1b[32m" + dmc-filter Subject < $a | head -n 1 + printf "\x1b[0m \t- " + dmc-filter To < $a | head -n 1 + printf " \t- " + dmc-filter Date < $a | head -n 1 + done + ;; +"-v"|"--version") + echo "dmc v${VERSION}" + ;; +"--help"|"-h") + echo ${HELP} + echo " -e account edit account information" + echo " -a name show addressbook email for contact" + echo " -A file add attachment to mail" + echo " -c cmd run command for \$DMC_ACCOUNT or acc.default daemon" + echo " -m addr subj create mail with default account" + echo " -s file send email" + echo " -l [mail/fold] list mails in folder or show mail" + echo " -v show version" + echo " -h show this help message" + echo " start start mail daemons" + echo " stop stop them all" + echo " push send mails in box/*/out" + echo " pull update box/*/in folders" + ;; +*) + echo "${HELP}" + ;; +esac + +exit 0 diff --git a/pack.c b/pack.c @@ -147,11 +147,13 @@ static void mime_unpack (int xtr) { } int main(int argc, char **argv) { - if (argc < 2 || !strcmp(argv[1], "-h")) { + if (argc < 2) { + mime_pack (argv+1, argc-1); + } else + if (!strcmp(argv[1], "-h")) { fprintf (stderr, "Usage: %s [-hlu | attachment1 attachment2...] < mail\n", argv[0]); return 1; - } - else if (!strcmp(argv[1], "-l")) + } else if (!strcmp(argv[1], "-l")) mime_unpack (0); else if (!strcmp(argv[1], "-u")) mime_unpack (1);