/*@ S-nail - a mail user agent derived from Berkeley Mail.
 *@ Implementation of mailcap.h.
 *@ TODO - We do not support the additional formats '%n' and '%F' (manual!).
 *@ TODO - We do not support handlers for multipart MIME parts (manual!).
 *@ TODO - We only support viewing/quoting (+ implications on fmt expansion).
 *@ TODO - With an on_loop_tick_event, trigger cache update once per loop max.
 *@ TODO   (or, if we ever get there, use a path_monitor: for all such!)
 *
 * Copyright (c) 2019 - 2020 Steffen (Daode) Nurpmeso <steffen@sdaoden.eu>.
 * SPDX-License-Identifier: ISC
 *
 * Permission to use, copy, modify, and/or distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */
#undef su_FILE
#define su_FILE mailcap
#define mx_SOURCE
#define mx_SOURCE_MAILCAP

#ifndef mx_HAVE_AMALGAMATION
# include "mx/nail.h"
#endif

su_EMPTY_FILE()
#ifdef mx_HAVE_MAILCAP
#include "su/cs.h"
#include "su/cs-dict.h"
#include "su/mem.h"

#include "mx/child.h"
#include "mx/cmd.h"
#include "mx/file-streams.h"
#include "mx/mime-type.h"

#include "mx/mailcap.h"
#include "su/code-in.h"

/* Whether we should try to place as much on a line as possible (Y).
 * Otherwise (X) we place commands on lines of their own */
#define a_MAILCAP_DUMP_SEP_INJ(X,Y) X /* Y */

/* Dictionary stores a_mailcap_hdl* list, not owned */
#define a_MAILCAP_CSD_FLAGS (su_CS_DICT_CASE | su_CS_DICT_HEAD_RESORT |\
      su_CS_DICT_ERR_PASS)
#define a_MAILCAP_CSD_TRESHOLD_SHIFT 3

/* Must be alphabetical */
enum a_mailcap_sfields{
   a_MAILCAP_SF_CMD,
   a_MAILCAP_SF_COMPOSE,
   a_MAILCAP_SF_COMPOSETYPED,
   a_MAILCAP_SF_DESCRIPTION,
   a_MAILCAP_SF_EDIT,
   a_MAILCAP_SF_NAMETEMPLATE,
   a_MAILCAP_SF_PRINT,
   a_MAILCAP_SF_TEST,
   a_MAILCAP_SF_X11_BITMAP
};
CTAV(a_MAILCAP_SF_CMD == 0);
enum {a_MAILCAP_SF_MAX = a_MAILCAP_SF_X11_BITMAP + 1};

/* sfields we really handle, less test (no format expansion in there) */
#define a_MAILCAP_SFIELD_SUPPORTED(X) \
   ((X) == a_MAILCAP_SF_CMD /*|| (X) == a_MAILCAP_SF_TEST*/)

enum a_mailcap_handler_flags{
   a_MAILCAP_F_TEXTUALNEWLINES = mx_MIMETYPE_HDL_MAX << 1,
   a_MAILCAP_F_TESTONCE = mx_MIMETYPE_HDL_MAX << 2,
   a_MAILCAP_F_TEST_ONCE_DONE = mx_MIMETYPE_HDL_MAX << 3,
   a_MAILCAP_F_TEST_ONCE_SUCCESS = mx_MIMETYPE_HDL_MAX << 4,
   a_MAILCAP_F_HAS_S_FORMAT = mx_MIMETYPE_HDL_MAX << 5, /* Somewhere a %s */
   a_MAILCAP_F_LAST_RESORT = mx_MIMETYPE_HDL_MAX << 6,
   a_MAILCAP_F_IGNORE = mx_MIMETYPE_HDL_MAX << 7
};
enum {a_MAILCAP_F_MAX = a_MAILCAP_F_IGNORE};
CTA(a_MAILCAP_F_MAX <= S32_MAX,
   "a_mailcap_hdl.mch_flags bit range excessed");

struct a_mailcap_hdl{
   struct a_mailcap_hdl *mch_next;
   BITENUM_IS(u32,mx_mimetype_handler_flags) mch_flags;
   u8 mch__pad[2];
   /* All strings are placed in successive memory after "self".
    * Since mch_cmd always exists 0 is the invalid offset for the rest.
    * The sum of all strings fits in S32_MAX.
    * sfield_has_format is a bitset */
   u16 mch_sfield_has_format;
   u32 mch_sfields[a_MAILCAP_SF_MAX];
};

struct a_mailcap_load_stack{
   char const *mcls_name;
   char const *mcls_name_quoted; /* Messages somewhat common, just do it. */
   FILE *mcls_fp;
   char const *mcls_type_subtype;
   struct str mcls_dat;
   struct str mcls_conti_dat;
   uz mcls_conti_len;
   /* Preparated handler; during preparation string data is temporarily stored
    * in .mcls_hdl_buf */
   struct a_mailcap_hdl mcls_hdl;
   struct n_string mcls_hdl_buf;
};

struct a_mailcap_flags{
   BITENUM_IS(u32,mx_mimetype_handler_flags) mcf_flags;
   char mcf_name[28];
};

static struct a_mailcap_flags const a_mailcap_flags[] = {
   /* In manual order */
   {mx_MIMETYPE_HDL_COPIOUSOUTPUT, "copiousoutput"},
   {mx_MIMETYPE_HDL_NEEDSTERM, "needsterminal"},
   {a_MAILCAP_F_TEXTUALNEWLINES, "textualnewlines"},
   {mx_MIMETYPE_HDL_ASYNC, "x-mailx-async"},
   {mx_MIMETYPE_HDL_NOQUOTE, "x-mailx-noquote"},
   {a_MAILCAP_F_TESTONCE, "x-mailx-test-once"},
   {mx_MIMETYPE_HDL_TMPF, "x-mailx-tmpfile"},
   {mx_MIMETYPE_HDL_TMPF_FILL, "x-mailx-tmpfile-fill"},
   {mx_MIMETYPE_HDL_TMPF_UNLINK, "x-mailx-tmpfile-unlink\0"},
   {a_MAILCAP_F_LAST_RESORT, "x-mailx-last-resort"},
   {a_MAILCAP_F_IGNORE, "x-mailx-ignore"}
};

static struct su_cs_dict *a_mailcap_dp, a_mailcap__d; /* XXX atexit _gut() */

/* We stop parsing and _gut(FAL0) on hard errors like NOMEM, OVERFLOW and IO.
 * The __parse*() series return is-error, with TRUM1 being a fatal one */
static void a_mailcap_create(void);

static boole a_mailcap__load_file(struct a_mailcap_load_stack *mclsp);
static boole a_mailcap__parse_line(struct a_mailcap_load_stack *mclsp,
      char *dp, uz dl);
static boole a_mailcap__parse_kv(struct a_mailcap_load_stack *mclsp,
      char *kp, char *vp);
static boole a_mailcap__parse_value(u32 sfield,
      struct a_mailcap_load_stack *mclsp, struct str *s);
static boole a_mailcap__parse_flag(struct a_mailcap_load_stack *mclsp,
      char *flag);
static boole a_mailcap__parse_create_hdl(struct a_mailcap_load_stack *mclsp,
      struct a_mailcap_hdl **ins_or_nil);

static void a_mailcap_gut(boole gut_dp);

/* */
static struct n_strlist *a_mailcap_dump(char const *cmdname, char const *key,
      void const *dat);

static void a_mailcap__dump_kv(u32 sfield, struct n_string *s, uz *llp,
      char const *pre, char const *vp);
static struct n_string *a_mailcap__dump_quote(struct n_string *s,
      char const *cp, boole quotequote);

/* Expand a command string with embedded formats */
static char const *a_mailcap_expand_formats(char const *format,
      struct mimepart const *mpp, char const *ct);

static void
a_mailcap_create(void){
   struct a_mailcap_load_stack mcls;
   char *cp_base, *cp;
   NYD_IN;

   a_mailcap_dp = su_cs_dict_set_treshold_shift(
            su_cs_dict_create(&a_mailcap__d, a_MAILCAP_CSD_FLAGS, NIL),
         a_MAILCAP_CSD_TRESHOLD_SHIFT);

   if(*(cp_base = UNCONST(char*,ok_vlook(MAILCAPS))) == '\0')
      goto jleave;

   su_mem_set(&mcls, 0, sizeof mcls);
   mx_fs_linepool_aquire(&mcls.mcls_dat.s, &mcls.mcls_dat.l);
   n_string_book(n_string_creat(&mcls.mcls_hdl_buf), 248); /* ovflw not yet */

   for(cp_base = savestr(cp_base);
         (cp = su_cs_sep_c(&cp_base, ':', TRU1)) != NIL;){
      if((cp = fexpand(cp, (FEXP_NOPROTO | FEXP_LOCAL_FILE | FEXP_NSHELL))
            ) == NIL)
         continue;

      mcls.mcls_name_quoted = n_shexp_quote_cp(cp, FAL0);
      if((mcls.mcls_fp = mx_fs_open(mcls.mcls_name = cp, "r")) == NIL){
         s32 eno;

         if((eno = su_err_no()) != su_ERR_NOENT)
            n_err(_("$MAILCAPS: cannot open %s: %s\n"),
               mcls.mcls_name_quoted, su_err_doc(eno));
         continue;
      }

      if(!a_mailcap__load_file(&mcls))
         cp = NIL;

      mx_fs_close(mcls.mcls_fp);

      if(cp == NIL){
         a_mailcap_gut(FAL0);
         break;
      }
   }

   if(mcls.mcls_conti_dat.s != NIL)
      mx_fs_linepool_release(mcls.mcls_conti_dat.s, mcls.mcls_conti_dat.l);
   mx_fs_linepool_release(mcls.mcls_dat.s, mcls.mcls_dat.l);

   n_string_gut(&mcls.mcls_hdl_buf);

jleave:
   NYD_OU;
}

static boole
a_mailcap__load_file(struct a_mailcap_load_stack *mclsp){
   enum{a_NONE, a_CONTI = 1u<<0, a_EOF = 1u<<1, a_NEWCONTI = 1u<<2};
   char const *emsg;
   uz len;
   u32 f;
   NYD2_IN;

   emsg = NIL;

   for(f = a_NONE;;){
      if(fgetline(&mclsp->mcls_dat.s, &mclsp->mcls_dat.l, NIL, &len,
            mclsp->mcls_fp, TRU1) == NIL){
         if(ferror(mclsp->mcls_fp)){
            emsg = N_("I/O error");
            goto jerr;
         }
         f |= a_EOF;
         if(f & a_CONTI)
            goto jconti_do;
         break;
      }
      ASSERT(len > 0);
      mclsp->mcls_dat.s[--len] = '\0';

      /* Is it a comment?  Must be in first column, cannot be continued */
      if(!(f & a_CONTI) && len > 0 && mclsp->mcls_dat.s[0] == '#')
         continue;

      /* Is it a continuation line?  It really is for an uneven number of \ */
      f &= ~a_NEWCONTI;
      if(len > 0 && mclsp->mcls_dat.s[len - 1] == '\\'){
         uz i, j;

         if(len == 1)
            continue;
         else for(j = 1, i = len - 1; i-- > 0; ++j)
            if(mclsp->mcls_dat.s[i] != '\\'){
               if(j & 1){
                  f |= a_NEWCONTI;
                  --len;
               }
               break;
            }
      }

      /* Necessary to create/append to continuation line storage? */
      if(f & (a_CONTI | a_NEWCONTI)){
         if(mclsp->mcls_conti_dat.s == NIL){
            mclsp->mcls_conti_dat = mclsp->mcls_dat;
            mclsp->mcls_conti_len = len;
            mx_fs_linepool_aquire(&mclsp->mcls_dat.s, &mclsp->mcls_dat.l);
         }else{
            if(!mx_fs_linepool_book(&mclsp->mcls_conti_dat.s,
                  &mclsp->mcls_conti_dat.l, mclsp->mcls_conti_len,
                  MAX(len, 256)))
               goto jetoolong;
            su_mem_copy(&mclsp->mcls_conti_dat.s[mclsp->mcls_conti_len],
               mclsp->mcls_dat.s, len +1);
            mclsp->mcls_conti_len += len;
         }
         f |= a_CONTI;

         if(f & a_NEWCONTI)
            continue;

jconti_do:
         /* C99 */{
            boole x;

            x = a_mailcap__parse_line(mclsp, mclsp->mcls_conti_dat.s,
                  mclsp->mcls_conti_len);
            /* Release the buffer to the linepool, like that we can swap it in
             * again the next time, shall this be necessary! */
            mx_fs_linepool_release(mclsp->mcls_conti_dat.s,
               mclsp->mcls_conti_dat.l);
            mclsp->mcls_conti_dat.s = NIL;

            switch(x){
            case FAL0: break;
            case TRU1: break;
            case TRUM1: goto jenomem;
            }
         }
         if((f ^= a_CONTI) & a_EOF)
            break;
      }else switch(a_mailcap__parse_line(mclsp, mclsp->mcls_dat.s, len)){
      case FAL0: break;
      case TRU1: break;
      case TRUM1: goto jenomem;
      }
   }

jleave:
   NYD2_OU;
   return (emsg == NIL);

jenomem:
   emsg = N_("out of memory");
   goto jerr;
jetoolong:
   su_state_err(su_STATE_ERR_OVERFLOW, (su_STATE_ERR_PASS |
      su_STATE_ERR_NOERRNO), _("$MAILCAPS: line too long"));
   emsg = N_("line too long");
jerr:
   n_err(_("$MAILCAPS: %s while loading %s\n"),
      V_(emsg), mclsp->mcls_name_quoted);
   goto jleave;
}

static boole
a_mailcap__parse_line(struct a_mailcap_load_stack *mclsp, char *dp, uz dl){
   struct str s;
   union {void *v; struct a_mailcap_hdl *mch; struct a_mailcap_hdl **pmch;} p;
   char *cp, *cp2, *key;
   uz rnd;
   boole rv;
   NYD2_IN;

   su_mem_set(&mclsp->mcls_hdl, 0, sizeof(mclsp->mcls_hdl));
   n_string_trunc(&mclsp->mcls_hdl_buf, 0);

   rv = TRU1;

   if(dl == 0)
      goto jleave;

   s.s = dp;
   s.l = dl;
   if(n_str_trim(&s, n_STR_TRIM_BOTH)->l == 0){
      if(n_poption & n_PO_D_V)
         n_err(_("$MAILCAPS: %s: line empty after whitespace removal "
               "(invalid RFC 1524 syntax)\n"), mclsp->mcls_name_quoted);
      goto jleave;
   }else if(s.l >= S32_MAX){
      /* As stated, the sum must fit in S32_MAX */
      rv = TRUM1;
      goto jleave;
   }
   (dp = s.s)[s.l] = '\0';

   rnd = 0;
   UNINIT(key, NIL);
   UNINIT(p.v, NIL);

   while((cp = su_cs_sep_escable_c(&dp, ';', FAL0)) != NIL){
      /* We do not allow empty fields, but there may be a trailing semicolon */
      if(*cp == '\0' && dp == NIL)
         break;

      /* First: TYPE/SUBTYPE; separate them first */
      if(++rnd == 1){
         if(*cp == '\0'){
            key = UNCONST(char*,N_("no MIME TYPE"));
            goto jerr;
         }else if((cp2 = su_cs_find_c(cp, '/')) == NIL){
jesubtype:
            n_err(_("$MAILCAPS: %s: missing SUBTYPE, assuming /* (any): %s\n"),
               mclsp->mcls_name_quoted, cp);
            cp2 = UNCONST(char*,n_star);
         }else{
            *cp2++ = '\0';
            if(*cp2 == '\0')
               goto jesubtype;
         }

         /* And unite for the real one */
         key = savecatsep(cp, '/', cp2);
         if(!mx_mimetype_is_valid(key, TRU1, TRU1)){
            cp = key;
            key = UNCONST(char*,N_("invalid MIME type"));
            goto jerr;
         }
         mclsp->mcls_type_subtype = key;

         if((p.v = su_cs_dict_lookup(a_mailcap_dp, key)) != NIL){
            while(p.mch->mch_next != NIL)
               p.mch = p.mch->mch_next;
            p.pmch = &p.mch->mch_next;
         }
      }
      /* The mandatory view command */
      else if(rnd == 2){
         s.l = su_cs_len(s.s = cp);
         n_str_trim(&s, n_STR_TRIM_BOTH);
         if((rv = a_mailcap__parse_value(a_MAILCAP_SF_CMD, mclsp, &s)))
            goto jleave;
      }
      /* An optional field */
      else{
         if(*cp == '\0'){
            if(n_poption & n_PO_D_V)
               n_err(_("$MAILCAPS: %s: ignoring empty optional field: %s\n"),
                  mclsp->mcls_name_quoted, key);
         }else if((cp2 = su_cs_find_c(cp, '=')) != NIL){
            *cp2++ = '\0';
            if((rv = a_mailcap__parse_kv(mclsp, cp, cp2)))
               goto jleave;
         }else if(a_mailcap__parse_flag(mclsp, cp))
            goto jleave;
      }
   }

   rv = a_mailcap__parse_create_hdl(mclsp, p.pmch);

jleave:
   NYD2_OU;
   return rv;

jerr:
   n_err(_("$MAILCAPS: %s: skip due to error: %s: %s\n"),
      mclsp->mcls_name_quoted, V_(key), cp);
   rv = TRU1;
   goto jleave;
}

static boole
a_mailcap__parse_kv(struct a_mailcap_load_stack *mclsp, char *kp, char *vp){
#undef a_X
#define a_X(X,Y) FIELD_INITI(su_CONCAT(a_MAILCAP_SF_, X) - 1) su_STRING(Y)
   static char const sfa[][16] = {
      a_X(COMPOSE, compose),
      a_X(COMPOSETYPED, composetyped),
      a_X(DESCRIPTION, description),
      a_X(EDIT, edit),
      a_X(NAMETEMPLATE, nametemplate),
      a_X(PRINT, print),
      a_X(TEST, test),
      a_X(X11_BITMAP, x11-bitmap)
   };
#undef a_X

   struct str s;
   char **cpp;
   char const *emsg, (*sfapp)[16];
   boole rv;
   NYD2_IN;

   /* Trim key and value */
   rv = TRU1;
   emsg = R(char*,1);
   cpp = &kp;
jredo:
   s.l = su_cs_len(s.s = *cpp);
   if(n_str_trim(&s, n_STR_TRIM_BOTH)->l == 0){
      emsg = (emsg == R(char*,1)) ? N_("ignored: empty key")
            : N_("ignored: empty value");
      goto jerr;
   }
   (*cpp = s.s)[s.l] = '\0';

   if(emsg == R(char*,1)){
      emsg = R(char*,-1);
      cpp = &vp;
      goto jredo;
   }

   emsg = NIL;

   /* Find keyword */
   for(sfapp = &sfa[0]; sfapp < &sfa[NELEM(sfa)]; ++sfapp){
      if(!su_cs_cmp_case(kp, *sfapp)){
         uz i;

         ASSERT(s.s == vp);
         i = P2UZ(sfapp - &sfa[0]) + 1;

         if((n_poption & n_PO_D_V) && mclsp->mcls_hdl.mch_sfields[i] != 0)
            n_err(_("$MAILCAPS: %s: %s: multiple %s fields\n"),
               mclsp->mcls_name_quoted, mclsp->mcls_type_subtype, kp);

         switch(i){
         default:
            break;
         case a_MAILCAP_SF_DESCRIPTION:
            /* This is "optionally quoted" */
            if(*vp == '"'){
               s.s = ++vp;
               --s.l;
               if(s.s[s.l - 1] == '"')
                  s.s[--s.l] = '\0';
               else
                  emsg = N_("unclosed quoted description");
            }
            break;

         case a_MAILCAP_SF_NAMETEMPLATE:{
            char *cp, c;

            if((cp = vp)[0] != '%' || cp[1] != 's'){
jentempl:
               emsg = N_("unsatisfied constraints, ignoring nametemplate");
               goto jerr;
            }
            for(cp += 2; (c = *cp++) != '\0';)
               if(!su_cs_is_alnum(c) && c != '_' && c != '.')
                  goto jentempl;
            }
            break;
         }

         mclsp->mcls_hdl.mch_sfields[i] = mclsp->mcls_hdl_buf.s_len;
         if((rv = a_mailcap__parse_value(i, mclsp, &s)))
            mclsp->mcls_hdl.mch_sfields[i] = 0;

         if(emsg != NIL)
            goto jerr;
         goto jleave;
      }
   }

   if((rv = (kp[0] != 'x' && kp[0] != '\0' && kp[1] != '-')) ||
         (n_poption & n_PO_D_V)){
      emsg = N_("ignored unknown string/command");
      goto jerr;
   }

   rv = FAL0;
jleave:
   NYD2_OU;
   return rv;

jerr:
   /* I18N: FILENAME: TYPE/SUBTYPE: ERROR MSG: key = value */
   n_err(_("$MAILCAPS: %s: %s: %s: %s = %s\n"),
      mclsp->mcls_name_quoted, mclsp->mcls_type_subtype, V_(emsg), kp, vp);
   goto jleave;
}

static boole
a_mailcap__parse_value(u32 sfield, struct a_mailcap_load_stack *mclsp,
      struct str *s){
   char *cp2, *cp3, c;
   boole rv;
   NYD2_IN;

   if(S(uz,S32_MAX) - mclsp->mcls_hdl_buf.s_len <= s->l +1){
      rv = TRUM1;
      goto jleave;
   }

   rv = FAL0;

   /* Take over unless we see a format, then branch to more expensive code */
   for(cp2 = cp3 = s->s;;){
      c = *cp2++;
      if(c == '\\')
         c = *cp2++;
      else if(c == '%'){
         if(*cp2 == '%')
            ++cp2;
         else{
            --cp2;
            goto jfmt;
         }
      }

      if((*cp3++ = c) == '\0')
         break;
   }

   n_string_push_c(n_string_push_buf(&mclsp->mcls_hdl_buf,
      s->s, P2UZ(cp3 - s->s)), '\0');

jleave:
   NYD2_OU;
   return rv;

jfmt:{
   char *lbuf;

   /* C99 */{
      uz i;

      lbuf = n_lofi_alloc(s->l * 2);
      i = P2UZ(cp3 - s->s);
      su_mem_copy(lbuf, s->s, i);
      cp3 = &lbuf[i];
   }

   for(;;){
      c = *cp2++;
      if(c == '\\')
         c = *cp2++;
      else if(c == '%'){
         switch((c = *cp2++)){
         case '{':
            if(su_cs_find_c(cp2, '}') == NIL){
               n_err(_("$MAILCAPS: %s: %s: unclosed %%{ format: %s\n"),
                  mclsp->mcls_name_quoted, mclsp->mcls_type_subtype, s->s);
               /* We need to skip the entire thing if we are incapable to
                * satisfy the format request when invoking a command */
               if(a_MAILCAP_SFIELD_SUPPORTED(sfield)){
                  rv = TRU1;
                  goto jfmt_leave;
               }
               goto jquote;
            }
            /* FALLTHRU */
            if(0){
         case 's':
               if(sfield == a_MAILCAP_SF_TEST){ /* XXX only view/quote */
                  n_err(_("$MAILCAPS: %s: %s: %%s format cannot be used in "
                        "the \"test\" field\n"),
                     mclsp->mcls_name_quoted, mclsp->mcls_type_subtype, s->s);
                  rv = TRU1;
                  goto jfmt_leave;
               }
               /* xxx Very primitive user-used-false-quotes check */
               if((n_poption & n_PO_D_V) && (*cp2 == '"' || *cp2 == '\''))
                  n_err(_("$MAILCAPS: %s: %s: (maybe!) "
                        "%%s must not be quoted: %s\n"),
                     mclsp->mcls_name_quoted, mclsp->mcls_type_subtype, s->s);

               mclsp->mcls_hdl.mch_flags |= a_MAILCAP_F_HAS_S_FORMAT;
            }
            /* FALLTHRU */
         case 't':
            mclsp->mcls_hdl.mch_sfield_has_format |= 1u << sfield;
            *cp3++ = '\0';
            break;

         case 'n':
            /* FALLTHRU */
         case 'F':
            n_err(_("$MAILCAPS: %s: %s: unsupported format %%%c\n"),
               mclsp->mcls_name_quoted, mclsp->mcls_type_subtype, c);
            /* We need to skip the entire thing if we are incapable to satisfy
             * the format request when invoking a command */
            if(a_MAILCAP_SFIELD_SUPPORTED(sfield)){
               rv = TRU1;
               goto jfmt_leave;
            }
            /* Since it is actually ignored, do not care, do not quote */
            mclsp->mcls_hdl.mch_sfield_has_format |= 1u << sfield;
            *cp3++ = '\0';
            break;

         default:
            n_err(_("$MAILCAPS: %s: %s: invalid format %%%c, "
                  "should be quoted: \\%%%c\n"),
               mclsp->mcls_name_quoted, mclsp->mcls_type_subtype, c, c);
jquote:
            --cp2;
            c = '%';
            break;
         }
      }

      if((*cp3++ = c) == '\0')
         break;
   }

   n_string_push_c(n_string_push_buf(&mclsp->mcls_hdl_buf,
      lbuf, P2UZ(cp3 - lbuf)), '\0');

jfmt_leave:
   n_lofi_free(lbuf);
   }
   goto jleave;
}

static boole
a_mailcap__parse_flag(struct a_mailcap_load_stack *mclsp, char *flag){
   struct a_mailcap_flags const *fap;
   boole rv;
   NYD2_IN;

   rv = FAL0;

   for(fap = &a_mailcap_flags[0];
         fap < &a_mailcap_flags[NELEM(a_mailcap_flags)]; ++fap)
      if(!su_cs_cmp_case(flag, fap->mcf_name)){
         if((n_poption & n_PO_D_V) &&
               (mclsp->mcls_hdl.mch_flags & fap->mcf_flags))
            n_err(_("$MAILCAPS: %s: %s: multiple %s flags\n"),
               mclsp->mcls_name_quoted, mclsp->mcls_type_subtype, flag);
         mclsp->mcls_hdl.mch_flags |= fap->mcf_flags;
         goto jleave;
      }

   if((rv = (flag[0] != 'x' && flag[0] != '\0' && flag[1] != '-')) ||
         (n_poption & n_PO_D_V))
      n_err(_("$MAILCAPS: %s: %s: ignored unknown flag: %s\n"),
         mclsp->mcls_name_quoted, mclsp->mcls_type_subtype, flag);

jleave:
   NYD2_OU;
   return rv;
}

static boole
a_mailcap__parse_create_hdl(struct a_mailcap_load_stack *mclsp,
      struct a_mailcap_hdl **ins_or_nil){
   struct a_mailcap_hdl *mchp;
   char const *emsg;
   u32 f;
   boole rv;
   NYD2_IN;

   rv = TRU1;

   /* Flag implications */

   f = mclsp->mcls_hdl.mch_flags;

   if(f & (mx_MIMETYPE_HDL_TMPF_FILL | mx_MIMETYPE_HDL_TMPF_UNLINK))
      f |= mx_MIMETYPE_HDL_TMPF;

   if(f & mx_MIMETYPE_HDL_ASYNC){
      if(f & mx_MIMETYPE_HDL_COPIOUSOUTPUT){
         emsg = N_("cannot use x-mailx-async and copiousoutput");
         goto jerr;
      }
      if(f & mx_MIMETYPE_HDL_TMPF_UNLINK){
         emsg = N_("cannot use x-mailx-async and x-mailx-tmpfile-unlink");
         goto jerr;
      }
   }

   if(f & mx_MIMETYPE_HDL_NEEDSTERM){
      if(f & mx_MIMETYPE_HDL_COPIOUSOUTPUT){
         emsg = N_("cannot use needsterminal and copiousoutput");
         goto jerr;
      }
      if(f & mx_MIMETYPE_HDL_ASYNC){
         emsg = N_("cannot use needsterminal and x-mailx-async");
         goto jerr;
      }
   }

   /* Mailcap implications */

   if(mclsp->mcls_hdl.mch_sfields[a_MAILCAP_SF_NAMETEMPLATE] != 0){
      if(f & a_MAILCAP_F_HAS_S_FORMAT)
         f |= mx_MIMETYPE_HDL_TMPF_NAMETMPL |
               mx_MIMETYPE_HDL_TMPF_NAMETMPL_SUFFIX;
      else
         n_err(_("$MAILCAPS: %s: %s: no %%s format, ignoring nametemplate\n"),
            mclsp->mcls_name_quoted, mclsp->mcls_type_subtype);
   }

   if(f & mx_MIMETYPE_HDL_TMPF){
      /* Not with any %s */
      if(f & a_MAILCAP_F_HAS_S_FORMAT){
         emsg = N_("cannot use x-mailx-tmpfile if formats use %s");
         goto jerr;
      }
   }

   mclsp->mcls_hdl.mch_flags = f;

   /* Since all strings altogether fit in S32_MAX, allocate one big chunk */
   rv = TRUM1;

   mchp = S(struct a_mailcap_hdl*,su_CALLOC(sizeof(*mchp) +
            mclsp->mcls_hdl_buf.s_len +1));
   if(mchp == NIL)
      goto jleave;

   su_mem_copy(mchp, &mclsp->mcls_hdl, sizeof(mclsp->mcls_hdl));
   su_mem_copy(&mchp[1], n_string_cp(&mclsp->mcls_hdl_buf),
      mclsp->mcls_hdl_buf.s_len +1);

   rv = FAL0;

   if(ins_or_nil != NIL)
      *ins_or_nil = mchp;
   else if(su_cs_dict_insert(a_mailcap_dp, mclsp->mcls_type_subtype, mchp) > 0)
      rv = TRUM1;

jleave:
   NYD2_OU;
   return rv;

jerr:
   UNUSED(emsg);
   n_err(_("$MAILCAPS: %s: %s: %s\n"),
      mclsp->mcls_name_quoted, mclsp->mcls_type_subtype, V_(emsg));
   goto jleave;
}

static void
a_mailcap_gut(boole gut_dp){
   NYD2_IN;

   if(a_mailcap_dp != NIL){
      struct su_cs_dict_view csdv;

      su_CS_DICT_FOREACH(a_mailcap_dp, &csdv){
         struct a_mailcap_hdl *mchp, *tmp;

         for(mchp = S(struct a_mailcap_hdl*,su_cs_dict_view_data(&csdv));
               mchp != NIL;){
            tmp = mchp;
            mchp = mchp->mch_next;
            su_FREE(tmp);
         }
      }

      if(gut_dp){
         su_cs_dict_gut(a_mailcap_dp);
         a_mailcap_dp = NIL;
      }else
         su_cs_dict_clear(a_mailcap_dp);
   }

   NYD2_OU;
}

static struct n_strlist *
a_mailcap_dump(char const *cmdname, char const *key, void const *dat){
   /* XXX real strlist + str_to_fmt() */
#undef a_X
#define a_X(X,Y) FIELD_INITI(su_CONCAT(a_MAILCAP_SF_, X)) Y
   static char const sfa[][20] = {
      a_X(CMD, " "),
      a_X(COMPOSE, " compose = "),
      a_X(COMPOSETYPED, " composetyped = \0"),
      a_X(DESCRIPTION, " description = "),
      a_X(EDIT, " edit = "),
      a_X(NAMETEMPLATE, " nametemplate = "),
      a_X(PRINT, " print = "),
      a_X(TEST, " test = "),
      a_X(X11_BITMAP, " x11-bitmap = ")
   };
#undef a_X

   struct n_string s_b, *s;
   struct a_mailcap_hdl const *mchp;
   struct n_strlist *slp;
   NYD2_IN;
   UNUSED(cmdname);

   s = n_string_book(n_string_creat_auto(&s_b), 127);
   s = n_string_resize(s, n_STRLIST_PLAIN_SIZE());

   for(mchp = S(struct a_mailcap_hdl const*,dat); mchp != NIL;
         mchp = mchp->mch_next){
      uz i, lo, lx;
      char const *buf;

      if(S(void const*,mchp) != dat)
         s = n_string_push_c(s, '\n');

      lo = i = su_cs_len(key);
      s = n_string_push_buf(s, key, i);

      buf = S(char const*,&mchp[1]);
      for(i = a_MAILCAP_SF_CMD; i < NELEM(sfa); ++i)
         if(i == a_MAILCAP_SF_CMD || mchp->mch_sfields[i] > 0)
            a_mailcap__dump_kv(i, s, &lo, sfa[i], &buf[mchp->mch_sfields[i]]);

      if(mchp->mch_flags != 0){
         a_MAILCAP_DUMP_SEP_INJ(boole any = FAL0, (void)0);

         for(i = 0; i < NELEM(a_mailcap_flags); ++i){
            if(mchp->mch_flags & a_mailcap_flags[i].mcf_flags){
               uz j;

               s = n_string_push_c(s, ';');
               ++lo;
               j = su_cs_len(a_mailcap_flags[i].mcf_name);
               if(a_MAILCAP_DUMP_SEP_INJ(!any, FAL0) || lo + j >= 76){
                  s = n_string_push_buf(s, "\\\n ", sizeof("\\\n ") -1);
                  lo = 1;
               }
               lx = s->s_len;
               s = n_string_push_c(s, ' ');
               s = n_string_push_buf(s, a_mailcap_flags[i].mcf_name, j);
               lo += s->s_len - lx;
               a_MAILCAP_DUMP_SEP_INJ(any = TRU1, (void)0);
            }
         }
      }
   }

   s = n_string_push_c(s, '\n');

   n_string_cp(s);

   slp = R(struct n_strlist*,S(void*,s->s_dat));
   /* xxx Should we assert alignment constraint of slp is satisfied?
    * xxx Should be, heap memory with alignment < sizeof(void*) bitter? */
   slp->sl_next = NIL;
   slp->sl_len = s->s_len;
   n_string_drop_ownership(s);

   NYD2_OU;
   return slp;
}

static void
a_mailcap__dump_kv(u32 sfield, struct n_string *s, uz *llp, char const *pre,
      char const *vp){
   boole quote;
   uz lo, prel, i, lx;
   NYD2_IN;

   lo = *llp;
   prel = su_cs_len(pre);

   s = n_string_push_c(s, ';');
   ++lo;

   for(i = 0;; ++i)
      if(vp[i] == '\0' && vp[i + 1] == '\0')
         break;
   /* An empty command is an error if no other field follows, a condition we do
    * not know here, so put something; instead of a dummy field, put success */
   if(i == 0 && sfield == a_MAILCAP_SF_CMD){
      vp = ":\0\0";
      i = 1;
   }

   if(a_MAILCAP_DUMP_SEP_INJ(sfield != a_MAILCAP_SF_CMD || lo + prel + i >= 75,
         lo + prel + i >= 72)){
      s = n_string_push_buf(s, "\\\n ", sizeof("\\\n ") -1);
      lo = 1;
   }

   quote = (sfield == a_MAILCAP_SF_DESCRIPTION);

   lx = s->s_len;
   s = n_string_push_buf(s, pre, prel);

   if(quote && i > 0 && (su_cs_is_space(vp[0]) || su_cs_is_space(vp[i - 1])))
      s = n_string_push_c(s, '"');
   else
      quote = FAL0;
   s = a_mailcap__dump_quote(s, vp, quote);
   if(quote)
      s = n_string_push_c(s, '"');

   lo += s->s_len - lx;
   *llp = lo;
   NYD2_OU;
}

static struct n_string *
a_mailcap__dump_quote(struct n_string *s, char const *cp, boole quotequote){
   char c;
   NYD2_IN;

   for(;;){
      if((c = *cp++) == '\0'){
         if((c = *cp++) == '\0')
            break;
         s = n_string_push_c(s, '%');
      }else if(c == ';' || c == '\\' || c == '%' || (c == '"' && quotequote))
         s = n_string_push_c(s, '\\');
      s = n_string_push_c(s, c);
   }

   NYD2_OU;
   return s;
}

static char const *
a_mailcap_expand_formats(char const *format, struct mimepart const *mpp,
      char const *ct){
   struct n_string s_b, *s = &s_b;
   char const *cp, *xp;
   NYD2_IN;

   s = n_string_creat_auto(s);
   s = n_string_book(s, 128);

   for(cp = format;;){
      char c;

      if((c = *cp++) == '\0'){
         if((c = *cp++) == '\0')
            break;

         switch(c){
         case '{':
            s = n_string_push_c(s, '\'');
            xp = su_cs_find_c(cp, '}');
            ASSERT(xp != NIL); /* (parser) */
            xp = savestrbuf(cp, P2UZ(xp - cp));
            if((xp = mime_param_get(xp, mpp->m_ct_type)) != NIL){
               /* XXX Maybe we should simply shell quote that thing? */
               while((c = *xp++) != '\0'){
                  if(c != '\'')
                     s = n_string_push_c(s, c);
                  else
                     s = n_string_push_cp(s, "'\"'\"'");
               }
            }
            s = n_string_push_c(s, '\'');
            break;
         case 's':
            /* For that we leave the actual expansion up to $SHELL.
             * See XXX comment in mx_mailcap_handler(), however */
            s = n_string_push_cp(s,
                  "\"${" n_PIPEENV_FILENAME_TEMPORARY "}\"");
            break;
         case 't':
            s = n_string_push_cp(s, ct);
            break;
         }
      }else
         s = n_string_push_c(s, c);
   }

   cp = n_string_cp(s);
   n_string_drop_ownership(s);

   NYD2_OU;
   return cp;
}

int
c_mailcap(void *vp){
   boole load_only;
   char **argv;
   NYD_IN;

   argv = vp;

   load_only = FAL0;
   if(*argv == NIL)
      goto jlist;
   if(argv[1] != NIL)
      goto jerr;
   if(su_cs_starts_with_case("show", *argv))
      goto jlist;

   load_only = TRU1;
   if(su_cs_starts_with_case("load", *argv))
      goto jlist;
   if(su_cs_starts_with_case("clear", *argv)){
      a_mailcap_gut(TRU1);
      goto jleave;
   }

jerr:
   mx_cmd_print_synopsis(mx_cmd_firstfit("mailcap"), NIL);
   vp = NIL;
jleave:
   NYD_OU;
   return (vp == NIL ? n_EXIT_ERR : n_EXIT_OK);

jlist:
   if(a_mailcap_dp == NIL)
      a_mailcap_create();

   if(!load_only){
      struct n_strlist *slp;

      slp = NIL;
      if(!(mx_xy_dump_dict("mailcap", a_mailcap_dp, &slp, NIL,
               &a_mailcap_dump) &&
            mx_page_or_print_strlist("mailcap", slp, TRU1)))
         vp = NIL;
   }
   goto jleave;
}

boole
mx_mailcap_handler(struct mx_mimetype_handler *mthp, char const *ct,
      enum sendaction action, struct mimepart const *mpp){
   union {void *v; char const *c; struct a_mailcap_hdl *mch;} p;
   boole wildcard;
   struct a_mailcap_hdl *mchp;
   NYD_IN;
   UNUSED(mpp);

   mchp = NIL;

   /* For now we support that only, too */
   ASSERT_NYD(action == SEND_QUOTE || action == SEND_QUOTE_ALL ||
      action == SEND_TODISP || action == SEND_TODISP_ALL ||
      action == SEND_TODISP_PARTS);

   if(ok_blook(mailcap_disable))
      goto jleave;

   if(a_mailcap_dp == NIL)
      a_mailcap_create();
   if(su_cs_dict_count(a_mailcap_dp) == 0)
      goto jleave;

   /* Walk over the list of handlers and check whether one fits */
   wildcard = FAL0;
   p.v = su_cs_dict_lookup(a_mailcap_dp, ct);
jagain:
   for(; p.mch != NIL; p.mch = p.mch->mch_next){
      u32 f;

      f = p.mch->mch_flags;

      if(f & a_MAILCAP_F_IGNORE)
         continue;

      if(action == SEND_TODISP || action == SEND_TODISP_ALL){
         /*if(f & mx_MIMETYPE_HDL_ASYNC)
          *   continue;*/
         if(!(f & mx_MIMETYPE_HDL_COPIOUSOUTPUT))
            continue;
      }else if(action == SEND_QUOTE || action == SEND_QUOTE_ALL){
         if(f & mx_MIMETYPE_HDL_NOQUOTE)
            continue;
         /*if(f & mx_MIMETYPE_HDL_ASYNC)
          *   continue;*/
         if(f & mx_MIMETYPE_HDL_NEEDSTERM) /* XXX for now */
            continue;
         if(!(f & mx_MIMETYPE_HDL_COPIOUSOUTPUT)) /* xxx for now */
            continue;
      }else{
         /* `mimeview' */
      }

      /* Flags seem to fit, do we need to test? */
      if(p.mch->mch_sfields[a_MAILCAP_SF_TEST] != 0){
         if(!(f & a_MAILCAP_F_TESTONCE) || !(f & a_MAILCAP_F_TEST_ONCE_DONE)){
            struct mx_child_ctx cc;

            mx_child_ctx_setup(&cc);
            cc.cc_flags = mx_CHILD_RUN_WAIT_LIFE;
            cc.cc_fds[0] = cc.cc_fds[1] = mx_CHILD_FD_NULL;
            cc.cc_cmd = ok_vlook(SHELL);
            cc.cc_args[0] = "-c";
            cc.cc_args[1] = &R(char const*,&p.mch[1]
                  )[p.mch->mch_sfields[a_MAILCAP_SF_TEST]];
            if(p.mch->mch_sfield_has_format & (1u << a_MAILCAP_SF_TEST))
               cc.cc_args[1] = a_mailcap_expand_formats(cc.cc_args[1],
                     mpp, ct);

            if(mx_child_run(&cc) && cc.cc_exit_status == n_EXIT_OK)
               f |= a_MAILCAP_F_TEST_ONCE_SUCCESS;

            if(f & a_MAILCAP_F_TESTONCE){
               f |= a_MAILCAP_F_TEST_ONCE_DONE;
               p.mch->mch_flags = f;
            }
         }

         if(!(f & a_MAILCAP_F_TEST_ONCE_SUCCESS))
            continue;
      }

      /* That one shall be it */
      if(f & a_MAILCAP_F_HAS_S_FORMAT){
         f |= mx_MIMETYPE_HDL_TMPF | mx_MIMETYPE_HDL_TMPF_FILL;
         if(!(f & mx_MIMETYPE_HDL_ASYNC))
            f |= mx_MIMETYPE_HDL_TMPF_UNLINK;
      }
      f |= mx_MIMETYPE_HDL_CMD;
      mthp->mth_flags = f;

      /* XXX We could use a different policy where the handler has a callback
       * XXX mechanism that is called when the handler's environment is fully
       * XXX setup; mailcap could use that to expand mth_shell_cmd.
       * XXX For now simply embed MAILX_FILENAME_TEMPORARY, and leave expansion
       * XXX up to the shell */
      mthp->mth_shell_cmd = &R(char const*,&p.mch[1]
            )[p.mch->mch_sfields[a_MAILCAP_SF_CMD]];
      if(p.mch->mch_sfield_has_format & (1u << a_MAILCAP_SF_CMD))
         mthp->mth_shell_cmd = a_mailcap_expand_formats(mthp->mth_shell_cmd,
               mpp, ct);

      if(p.mch->mch_sfields[a_MAILCAP_SF_NAMETEMPLATE] != 0){
         mthp->mth_tmpf_nametmpl = &R(char const*,&p.mch[1]
               )[p.mch->mch_sfields[a_MAILCAP_SF_NAMETEMPLATE]];
         ASSERT(mthp->mth_tmpf_nametmpl[0] == '\0' &&
            mthp->mth_tmpf_nametmpl[1] == 's');
         mthp->mth_tmpf_nametmpl += 2;
      }

      mchp = p.mch;
      goto jleave;
   }

   /* Direct match, otherwise try wildcard match? */
   if(!wildcard && (p.c = su_cs_find_c(ct, '/')) != NIL){
      char *cp;
      uz i;

      wildcard = TRU1;
      cp = n_lofi_alloc((i = P2UZ(p.c - ct)) + 2 +1);

      su_mem_copy(cp, ct, i);
      cp[i++] = '/';
      cp[i++] = '*';
      cp[i++] = '\0';
      p.v = su_cs_dict_lookup(a_mailcap_dp, cp);

      n_lofi_free(cp);

      if(p.v != NIL)
         goto jagain;
   }

jleave:
   NYD_OU;
   return (mchp == NIL ? FAL0
      : (mchp->mch_flags & a_MAILCAP_F_LAST_RESORT ? TRUM1 : TRU1));
}

#include "su/code-ou.h"
#endif /* mx_HAVE_MAILCAP */
/* s-it-mode */
