/* RFC1524 (mailcap file) implementation */ /* This file contains various functions for implementing a fair subset of * rfc1524. * * The rfc1524 defines a format for the Multimedia Mail Configuration, which is * the standard mailcap file format under Unix which specifies what external * programs should be used to view/compose/edit multimedia files based on * content type. * * Copyright (C) 1996-2000 Michael R. Elkins * Copyright (c) 2002-2004 The ELinks project * * This file was hijacked from the Mutt project * (version 1.4) on Saturday the 7th December 2002. It has been heavily * elinksified. */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include #include #include #include #include "elinks.h" #include "config/options.h" #include "intl/gettext/libintl.h" #include "main/module.h" #include "mime/backend/common.h" #include "mime/backend/mailcap.h" #include "mime/mime.h" #include "osdep/osdep.h" /* For exe() */ #include "session/session.h" #include "util/file.h" #include "util/hash.h" #include "util/lists.h" #include "util/memory.h" #include "util/string.h" struct mailcap_hash_item { /* The entries associated with the type */ struct list_head entries; /* -> struct mailcap_entry */ /* The content type of all @entries. Must be last! */ unsigned char type[1]; }; struct mailcap_entry { LIST_HEAD(struct mailcap_entry); /* To verify if command qualifies. Cannot contain %s formats. */ unsigned char *testcommand; /* Used to inform the user of the type or handler. */ unsigned char *description; /* Used to determine between an exact match and a wildtype match. Lower * is better. Increased for each sourced file. */ unsigned int priority; /* Whether the program "blocks" the term. */ unsigned int needsterminal:1; /* If "| ${PAGER}" should be added. It would of course be better to * pipe the output into a buffer and let ELinks display it but this * will have to do for now. */ unsigned int copiousoutput:1; /* The 'raw' unformatted (view)command from the mailcap files. */ unsigned char command[1]; }; enum mailcap_option { MAILCAP_TREE, MAILCAP_ENABLE, MAILCAP_PATH, MAILCAP_ASK, MAILCAP_DESCRIPTION, MAILCAP_PRIORITIZE, MAILCAP_OPTIONS }; static struct option_info mailcap_options[] = { INIT_OPT_TREE("mime", N_("Mailcap"), "mailcap", 0, N_("Options for mailcap support.")), INIT_OPT_BOOL("mime.mailcap", N_("Enable"), "enable", 0, 1, N_("Enable mailcap support.")), INIT_OPT_STRING("mime.mailcap", N_("Path"), "path", 0, DEFAULT_MAILCAP_PATH, N_("Mailcap search path. Colon-separated list of files.\n" "Leave as \"\" to use MAILCAP environment variable instead.")), INIT_OPT_BOOL("mime.mailcap", N_("Ask before opening"), "ask", 0, 1, N_("Ask before using the handlers defined by mailcap.")), INIT_OPT_INT("mime.mailcap", N_("Type query string"), "description", 0, 0, 2, 0, N_("Type of description to show in \"what to do with this file\"\n" "query dialog:\n" "0 is show \"mailcap\"\n" "1 is show program to be run\n" "2 is show mailcap description field if any; \"mailcap\" otherwise")), INIT_OPT_BOOL("mime.mailcap", N_("Prioritize entries by file"), "prioritize", 0, 1, N_("Prioritize entries by the order of the files in the mailcap\n" "path. This means that wildcard entries (like: image/*) will\n" "also be checked before deciding the handler.")), NULL_OPTION_INFO, }; #define get_opt_mailcap(which) mailcap_options[(which)].option #define get_mailcap(which) get_opt_mailcap(which).value #define get_mailcap_ask() get_mailcap(MAILCAP_ASK).number #define get_mailcap_description() get_mailcap(MAILCAP_DESCRIPTION).number #define get_mailcap_enable() get_mailcap(MAILCAP_ENABLE).number #define get_mailcap_prioritize() get_mailcap(MAILCAP_PRIORITIZE).number #define get_mailcap_path() get_mailcap(MAILCAP_PATH).string /* State variables */ static struct hash *mailcap_map = NULL; static int mailcap_map_size = 0; static inline void done_mailcap_entry(struct mailcap_entry *entry) { if (!entry) return; mem_free_if(entry->testcommand); mem_free_if(entry->description); mem_free(entry); } /* Takes care of all initialization of mailcap entries. * Clear memory to make freeing it safer later and we get * needsterminal and copiousoutput initialized for free. */ static inline struct mailcap_entry * init_mailcap_entry(unsigned char *command, int priority) { struct mailcap_entry *entry; int commandlen = strlen(command); entry = mem_calloc(1, sizeof(*entry) + commandlen); if (!entry) return NULL; memcpy(entry->command, command, commandlen); entry->priority = priority; return entry; } static inline void add_mailcap_entry(struct mailcap_entry *entry, unsigned char *type, int typelen) { struct mailcap_hash_item *mitem; struct hash_item *item; /* Time to get the entry into the mailcap_map */ /* First check if the type is already checked in */ item = get_hash_item(mailcap_map, type, typelen); if (!item) { mitem = mem_alloc(sizeof(*mitem) + typelen); if (!mitem) { done_mailcap_entry(entry); return; } safe_strncpy(mitem->type, type, typelen + 1); init_list(mitem->entries); item = add_hash_item(mailcap_map, mitem->type, typelen, mitem); if (!item) { mem_free(mitem); done_mailcap_entry(entry); return; } } else if (item->value) { mitem = item->value; } else { done_mailcap_entry(entry); return; } add_to_list_end(mitem->entries, entry); mailcap_map_size++; } /* Parsing of a RFC1524 mailcap file */ /* The format is: * * base/type; command; extradefs * * type can be * for matching all; base with no /type is an implicit * wildcard; command contains a %s for the filename to pass, default to pipe on * stdin; extradefs are of the form: * * def1="definition"; def2="define \;"; * * line wraps with a \ at the end of the line, # for comments. */ /* TODO handle default pipe. Maybe by prepending "cat |" to the command. */ /* Returns a NULL terminated RFC 1524 field, while modifying @next to point * to the next field. */ static unsigned char * get_mailcap_field(unsigned char **next) { unsigned char *field; unsigned char *fieldend; if (!next || !*next) return NULL; field = *next; skip_space(field); fieldend = field; /* End field at the next occurence of ';' but not escaped '\;' */ do { /* Handle both if ';' is the first char or if it's escaped */ if (*fieldend == ';') fieldend++; fieldend = strchr(fieldend, ';'); } while (fieldend && *(fieldend-1) == '\\'); if (fieldend) { *fieldend = '\0'; *next = fieldend; fieldend--; (*next)++; skip_space(*next); } else { *next = NULL; fieldend = field + strlen(field) - 1; } /* Remove trailing whitespace */ while (field <= fieldend && isspace(*fieldend)) *fieldend-- = '\0'; return field; } /* Parses specific fields (ex: the '=TestCommand' part of 'test=TestCommand'). * Expects that @field is pointing right after the specifier (ex: 'test' * above). Allocates and returns a NULL terminated token, or NULL if parsing * fails. */ static unsigned char * get_mailcap_field_text(unsigned char *field) { skip_space(field); if (*field == '=') { field++; skip_space(field); return stracpy(field); } return NULL; } /* Parse optional extra definitions. Zero return value means syntax error */ static inline int parse_optional_fields(struct mailcap_entry *entry, unsigned char *line) { while (0xf131d5) { unsigned char *field = get_mailcap_field(&line); if (!field) break; if (!strncasecmp(field, "needsterminal", 13)) { entry->needsterminal = 1; } else if (!strncasecmp(field, "copiousoutput", 13)) { entry->copiousoutput = 1; } else if (!strncasecmp(field, "test", 4)) { entry->testcommand = get_mailcap_field_text(field + 4); if (!entry->testcommand) return 0; /* Find out wether testing requires filename */ for (field = entry->testcommand; *field; field++) if (*field == '%' && *(field+1) == 's') { mem_free(entry->testcommand); return 0; } } else if (!strncasecmp(field, "description", 11)) { entry->description = get_mailcap_field_text(field + 11); if (!entry->description) return 0; } } return 1; } /* Parses whole mailcap files line-by-line adding entries to the map * assigning them the given @priority */ static void parse_mailcap_file(unsigned char *filename, unsigned int priority) { FILE *file = fopen(filename, "rb"); unsigned char *line = NULL; size_t linelen = MAX_STR_LEN; int lineno = 1; if (!file) return; while ((line = file_read_line(line, &linelen, file, &lineno))) { struct mailcap_entry *entry; unsigned char *linepos; unsigned char *command; unsigned char *basetypeend; unsigned char *type; int typelen; /* Ignore comments */ if (*line == '#') continue; linepos = line; /* Get type */ type = get_mailcap_field(&linepos); if (!type) continue; /* Next field is the viewcommand */ command = get_mailcap_field(&linepos); if (!command) continue; entry = init_mailcap_entry(command, priority); if (!entry) continue; if (!parse_optional_fields(entry, linepos)) { done_mailcap_entry(entry); usrerror(gettext("Badly formated mailcap entry " "for type %s in \"%s\" line %d"), type, filename, lineno); continue; } basetypeend = strchr(type, '/'); typelen = strlen(type); if (!basetypeend) { unsigned char implicitwild[64]; if (typelen + 3 > sizeof(implicitwild)) { done_mailcap_entry(entry); continue; } memcpy(implicitwild, type, typelen); implicitwild[typelen++] = '/'; implicitwild[typelen++] = '*'; implicitwild[typelen++] = '\0'; add_mailcap_entry(entry, implicitwild, typelen); continue; } add_mailcap_entry(entry, type, typelen); } fclose(file); mem_free_if(line); /* Alloced by file_read_line() */ } /* When initializing the mailcap map/hash read, parse and build a hash mapping * content type to handlers. Map is built from a list of mailcap files. * * The RFC1524 specifies that a path of mailcap files should be used. * o First we check to see if the user supplied any in mime.mailcap.path * o Then we check the MAILCAP environment variable. * o Finally fall back to reasonable default */ static struct hash * init_mailcap_map(void) { unsigned char *path; unsigned int priority = 0; mailcap_map = init_hash(8, &strhash); if (!mailcap_map) return NULL; /* Try to setup mailcap_path */ path = get_mailcap_path(); if (!path || !*path) path = getenv("MAILCAP"); if (!path) path = DEFAULT_MAILCAP_PATH; while (*path) { unsigned char *filename = get_next_path_filename(&path, ':'); if (!filename) continue; parse_mailcap_file(filename, priority++); mem_free(filename); } return mailcap_map; } static void done_mailcap(struct module *module) { struct hash_item *item; int i; if (!mailcap_map) return; foreach_hash_item (item, *mailcap_map, i) { struct mailcap_hash_item *mitem = item->value; if (!mitem) continue; while (!list_empty(mitem->entries)) { struct mailcap_entry *entry = mitem->entries.next; del_from_list(entry); done_mailcap_entry(entry); } mem_free(mitem); } free_hash(mailcap_map); mailcap_map = NULL; mailcap_map_size = 0; } #ifndef TEST_MAILCAP static int change_hook_mailcap(struct session *ses, struct option *current, struct option *changed) { if (changed == &get_opt_mailcap(MAILCAP_PATH) || (changed == &get_opt_mailcap(MAILCAP_ENABLE) && !get_mailcap_enable())) { done_mailcap(&mailcap_mime_module); } return 0; } static void init_mailcap(struct module *module) { struct change_hook_info mimetypes_change_hooks[] = { { "mime.mailcap", change_hook_mailcap }, { NULL, NULL }, }; register_change_hooks(mimetypes_change_hooks); if (get_cmd_opt_bool("anonymous")) get_mailcap_enable() = 0; } #else #define init_mailcap NULL #endif /* TEST_MAILCAP */ /* The command semantics include the following: * * %s is the filename that contains the mail body data * %t is the content type, like text/plain * %{parameter} is replaced by the parameter value from the content-type * field * \% is % * * Unsupported RFC1524 parameters: these would probably require some doing * by Mutt, and can probably just be done by piping the message to metamail: * * %n is the integer number of sub-parts in the multipart * %F is "content-type filename" repeated for each sub-part * Only % is supported by subst_file() which is equivalent to %s. */ /* The formatting is postponed until the command is needed. This means * @type can be NULL. If '%t' is used in command we bail out. */ static unsigned char * format_command(unsigned char *command, unsigned char *type, int copiousoutput) { struct string cmd; if (!init_string(&cmd)) return NULL; while (*command) { unsigned char *start = command; while (*command && *command != '%' && *command != '\\') command++; if (start < command) add_bytes_to_string(&cmd, start, command - start); if (*command == '%') { command++; if (!*command) { done_string(&cmd); return NULL; } else if (*command == 's') { add_char_to_string(&cmd, '%'); } else if (*command == 't') { if (!type) { done_string(&cmd); return NULL; } add_to_string(&cmd, type); } command++; } else if (*command == '\\') { command++; if (*command) { add_char_to_string(&cmd, *command); command++; } } } if (copiousoutput) { unsigned char *pager = getenv("PAGER"); if (!pager && file_exists(DEFAULT_PAGER_PATH)) { pager = DEFAULT_PAGER_PATH; } else if (!pager && file_exists(DEFAULT_LESS_PATH)) { pager = DEFAULT_LESS_PATH; } else if (!pager && file_exists(DEFAULT_MORE_PATH)) { pager = DEFAULT_MORE_PATH; } if (pager) { add_char_to_string(&cmd, '|'); add_to_string(&cmd, pager); } } return cmd.source; } /* Returns first usable mailcap_entry from a list where @entry is the head. * Use of @filename is not supported (yet). */ static struct mailcap_entry * check_entries(struct mailcap_hash_item *item) { struct mailcap_entry *entry; foreach (entry, item->entries) { unsigned char *test; /* Accept current if no test is needed */ if (!entry->testcommand) return entry; /* We have to run the test command */ test = format_command(entry->testcommand, NULL, 0); if (test) { int exitcode = exe(test); mem_free(test); if (!exitcode) return entry; } } return NULL; } /* Attempts to find the given type in the mailcap association map. On success, * this returns the associated command, else NULL. Type is a string with * syntax '/' (ex: 'text/plain') * * First the given type is looked up. Then the given -type with added * wildcard '*' (ex: 'text/'). For each lookup all the associated * entries are checked/tested. * * The lookup supports testing on files. If no file is given (NULL) any tests * that need a file will be taken as failed. */ static struct mailcap_entry * get_mailcap_entry(unsigned char *type) { struct mailcap_entry *entry; struct hash_item *item; item = get_hash_item(mailcap_map, type, strlen(type)); /* Check list of entries */ entry = (item && item->value) ? check_entries(item->value) : NULL; if (!entry || get_mailcap_prioritize()) { /* The type lookup has either failed or we need to check * the priorities so get the wild card handler */ struct mailcap_entry *wildcard = NULL; unsigned char *wildpos = strchr(type, '/'); if (wildpos) { int wildlen = wildpos - type + 1; /* include '/' */ unsigned char *wildtype = memacpy(type, wildlen + 2); if (!wildtype) return NULL; wildtype[wildlen++] = '*'; wildtype[wildlen] = '\0'; item = get_hash_item(mailcap_map, wildtype, wildlen); mem_free(wildtype); if (item && item->value) wildcard = check_entries(item->value); } /* Use @wildcard if its priority is better or @entry is NULL */ if (wildcard && (!entry || (wildcard->priority < entry->priority))) entry = wildcard; } return entry; } static struct mime_handler * get_mime_handler_mailcap(unsigned char *type, int options) { struct mailcap_entry *entry; struct mime_handler *handler; unsigned char *program; int block; if (!get_mailcap_enable() || (!mailcap_map && !init_mailcap_map())) return NULL; entry = get_mailcap_entry(type); if (!entry) return NULL; program = format_command(entry->command, type, entry->copiousoutput); if (!program) return NULL; block = (entry->needsterminal || entry->copiousoutput); handler = init_mime_handler(program, entry->description, mailcap_mime_module.name, get_mailcap_ask(), block); mem_free(program); return handler; } struct mime_backend mailcap_mime_backend = { /* get_content_type: */ NULL, /* get_mime_handler: */ get_mime_handler_mailcap, }; /* Setup the exported module. */ struct module mailcap_mime_module = struct_module( /* name: */ N_("Mailcap"), /* options: */ mailcap_options, /* hooks: */ NULL, /* submodules: */ NULL, /* data: */ NULL, /* init: */ init_mailcap, /* done: */ done_mailcap ); #ifdef TEST_MAILCAP /* Some ugly shortcuts for getting defined symbols to work. */ int default_mime_backend, install_signal_handler, mimetypes_mime_backend, program; struct list_head terminals; void die(const char *msg, ...) { va_list args; if (msg) { va_start(args, msg); vfprintf(stderr, msg, args); fputs("\n", stderr); va_end(args); } exit(1); } int main(int argc, char *argv[]) { unsigned char *format = "description,ask,block,program"; int has_gotten = 0; int i; for (i = 1; i < argc; i++) { char *arg = argv[i]; if (strncmp(arg, "--", 2)) break; arg += 2; if (!strncmp(arg, "path", 4)) { arg += 4; if (*arg == '=') { arg++; get_mailcap_path() = arg; } else { i++; if (i >= argc) die("--path expects a parameter"); get_mailcap_path() = argv[i]; } done_mailcap(NULL); } else if (!strncmp(arg, "format", 6)) { arg += 6; if (*arg == '=') { arg++; format = arg; } else { i++; if (i >= argc) die("--format expects a parameter"); format = argv[i]; } } else if (!strncmp(arg, "get", 3)) { struct mime_handler *handler; arg += 3; if (*arg == '=') { arg++; } else { i++; if (i >= argc) die("--get expects a parameter"); arg = argv[i]; } if (has_gotten) printf("\n"); has_gotten = 1; printf("type: %s\n", arg); handler = get_mime_handler_mailcap(arg, 0); if (!handler) continue; if (strstr(format, "description")) printf("description: %s\n", handler->description); if (strstr(format, "ask")) printf("ask: %d\n", handler->ask); if (strstr(format, "block")) printf("block: %d\n", handler->block); if (strstr(format, "program")) printf("program: %s\n", handler->program); } else { die("Unknown argument '%s'", arg - 2); } } done_mailcap(NULL); return 0; } #endif /* TEST_MAILCAP */