1
0
mirror of https://github.com/rkd77/elinks.git synced 2024-11-04 08:17:17 -05:00
elinks/src/mime/backend/mailcap.c
2007-02-04 13:34:48 +02:00

785 lines
19 KiB
C

/* 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 <me@cs.hmc.edu>
* Copyright (c) 2002-2004 The ELinks project
*
* This file was hijacked from the Mutt project <URL:http://www.mutt.org>
* (version 1.4) on Saturday the 7th December 2002. It has been heavily
* elinksified. */
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#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 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);
}
/* 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_hash8();
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);
}
#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)
{
static const 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 0
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);
}
}
#endif
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 '<base>/<type>' (ex: 'text/plain')
*
* First the given type is looked up. Then the given <base>-type with added
* wildcard '*' (ex: 'text/<star>'). 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);
if (handler) handler->copiousoutput = entry->copiousoutput;
mem_free(program);
return handler;
}
const 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 */