From 9bf6d74e9cc5a7b6b7a62fdefd6d953d60a422db Mon Sep 17 00:00:00 2001 From: Kevin McCarthy Date: Sun, 13 Nov 2016 20:02:33 -0800 Subject: [PATCH] Compress patch from the neomutt repository. With the following changes: - po/de.po changes trimmed to just the compress additions. - Move the sample muttrc to contrib, and add it to the Makefile.am so it is distributed. Remove the sample vimrc. - Remove extra fluff from manual. Thanks to Roland Rosenfeld for the original patch, and to the NeoMutt team for their work cleaning up the patch. --- Makefile.am | 4 +- commands.c | 31 ++ compress.c | 918 +++++++++++++++++++++++++++++++++ compress.h | 29 ++ configure.ac | 9 + contrib/Makefile.am | 2 +- contrib/sample.muttrc-compress | 38 ++ curs_main.c | 5 + doc/Muttrc.head | 5 + doc/manual.xml.head | 276 ++++++++++ doc/muttrc.man.head | 19 +- hook.c | 12 + init.h | 5 + main.c | 6 + mutt.h | 9 + mx.c | 29 +- mx.h | 4 + po/POTFILES.in | 1 + po/de.po | 25 + status.c | 6 + 20 files changed, 1428 insertions(+), 5 deletions(-) create mode 100644 compress.c create mode 100644 compress.h create mode 100644 contrib/sample.muttrc-compress diff --git a/Makefile.am b/Makefile.am index 789c23ba..4bc2db47 100644 --- a/Makefile.am +++ b/Makefile.am @@ -54,7 +54,7 @@ DEFS=-DPKGDATADIR=\"$(pkgdatadir)\" -DSYSCONFDIR=\"$(sysconfdir)\" \ AM_CPPFLAGS=-I. -I$(top_srcdir) $(IMAP_INCLUDES) $(GPGME_CFLAGS) -Iintl -EXTRA_mutt_SOURCES = account.c bcache.c crypt-gpgme.c crypt-mod-pgp-classic.c \ +EXTRA_mutt_SOURCES = account.c bcache.c compress.c crypt-gpgme.c crypt-mod-pgp-classic.c \ crypt-mod-pgp-gpgme.c crypt-mod-smime-classic.c \ crypt-mod-smime-gpgme.c dotlock.c gnupgparse.c hcache.c md5.c \ mutt_idna.c mutt_sasl.c mutt_socket.c mutt_ssl.c mutt_ssl_gnutls.c \ @@ -65,7 +65,7 @@ EXTRA_mutt_SOURCES = account.c bcache.c crypt-gpgme.c crypt-mod-pgp-classic.c \ EXTRA_DIST = COPYRIGHT GPL OPS OPS.PGP OPS.CRYPT OPS.SMIME TODO UPDATING \ configure account.h \ - attach.h buffy.h charset.h copy.h crypthash.h dotlock.h functions.h gen_defs \ + attach.h buffy.h charset.h compress.h copy.h crypthash.h dotlock.h functions.h gen_defs \ globals.h hash.h history.h init.h keymap.h mutt_crypt.h \ mailbox.h mapping.h md5.h mime.h mutt.h mutt_curses.h mutt_menu.h \ mutt_regex.h mutt_sasl.h mutt_socket.h mutt_ssl.h mutt_tunnel.h \ diff --git a/commands.c b/commands.c index 42436231..25b22262 100644 --- a/commands.c +++ b/commands.c @@ -837,6 +837,16 @@ int mutt_save_message (HEADER *h, int delete, if (mx_open_mailbox (buf, MUTT_APPEND, &ctx) != NULL) { +#ifdef USE_COMPRESSED + /* If we're saving to a compressed mailbox, the stats won't be updated + * until the next open. Until then, improvise. */ + BUFFY *cm = NULL; + if (ctx.compress_info) + cm = mutt_find_mailbox (ctx.realpath); + /* We probably haven't been opened yet */ + if (cm && (cm->msg_count == 0)) + cm = NULL; +#endif if (h) { if (_mutt_save_message(h, &ctx, delete, decode, decrypt) != 0) @@ -844,6 +854,16 @@ int mutt_save_message (HEADER *h, int delete, mx_close_mailbox (&ctx, NULL); return -1; } +#ifdef USE_COMPRESSED + if (cm) + { + cm->msg_count++; + if (!h->read) + cm->msg_unread++; + if (h->flagged) + cm->msg_flagged++; + } +#endif } else { @@ -858,6 +878,17 @@ int mutt_save_message (HEADER *h, int delete, mx_close_mailbox (&ctx, NULL); return -1; } +#ifdef USE_COMPRESSED + if (cm) + { + HEADER *h = Context->hdrs[Context->v2r[i]]; + cm->msg_count++; + if (!h->read) + cm->msg_unread++; + if (h->flagged) + cm->msg_flagged++; + } +#endif } } } diff --git a/compress.c b/compress.c new file mode 100644 index 00000000..aadf0948 --- /dev/null +++ b/compress.c @@ -0,0 +1,918 @@ +/* Copyright (C) 1997 Alain Penders + * Copyright (C) 2016 Richard Russon + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#if HAVE_CONFIG_H +#include "config.h" +#endif + +#include +#include +#include +#include + +#include "mutt.h" +#include "mailbox.h" +#include "mutt_curses.h" +#include "mx.h" + +/* Notes: + * Any references to compressed files also apply to encrypted files. + * ctx->path == plaintext file + * ctx->realpath == compressed file + */ + +/** + * struct COMPRESS_INFO - Private data for compress + * + * This object gets attached to the mailbox's CONTEXT. + */ +typedef struct +{ + const char *append; /* append-hook command */ + const char *close; /* close-hook command */ + const char *open; /* open-hook command */ + off_t size; /* size of the compressed file */ + struct mx_ops *child_ops; /* callbacks of de-compressed file */ +} COMPRESS_INFO; + + +/** + * lock_mailbox - Try to lock a mailbox (exclusively) + * @ctx: Mailbox to lock + * @fp: File pointer to the mailbox file + * @excl: Lock exclusively? + * + * Try to (exclusively) lock the mailbox. If we succeed, then we mark the + * mailbox as locked. If we fail, but we didn't want exclusive rights, then + * the mailbox will be marked readonly. + * + * Returns: + * 1: Success (locked or readonly) + * 0: Error (can't lock the file) + */ +static int +lock_mailbox (CONTEXT *ctx, FILE *fp, int excl) +{ + if (!ctx || !fp) + return 0; + + int r = mx_lock_file (ctx->realpath, fileno (fp), excl, 1, 1); + + if (r == 0) + { + ctx->locked = 1; + } + else if (excl == 0) + { + ctx->readonly = 1; + return 1; + } + + return (r == 0); +} + +/** + * unlock_mailbox - Unlock a mailbox + * @ctx: Mailbox to unlock + * @fp: File pointer to mailbox file + * + * Unlock a mailbox previously locked by lock_mailbox(). + */ +static void +unlock_mailbox (CONTEXT *ctx, FILE *fp) +{ + if (!ctx || !fp) + return; + + if (!ctx->locked) + return; + + fflush (fp); + + mx_unlock_file (ctx->realpath, fileno (fp), 1); + ctx->locked = 0; +} + +/** + * setup_paths - Set the mailbox paths + * @ctx: Mailbox to modify + * + * Save the compressed filename in ctx->realpath. + * Create a temporary filename and put its name in ctx->path. + * + * Note: The temporary file is NOT created. + * Note: ctx->path will be freed by restore_path() + */ +static void +setup_paths (CONTEXT *ctx) +{ + if (!ctx) + return; + + char tmppath[_POSIX_PATH_MAX]; + + /* Setup the right paths */ + ctx->realpath = ctx->path; + + /* We will uncompress to /tmp */ + mutt_mktemp (tmppath, sizeof (tmppath)); + ctx->path = safe_strdup (tmppath); +} + +/** + * restore_path - Put back the original mailbox name + * @ctx: Mailbox to modify + * + * When we use a compressed mailbox, we change the CONTEXT to refer to the + * uncompressed file. We store the original name in ctx->realpath. + * ctx->path = "/tmp/mailbox" + * ctx->realpath = "mailbox.gz" + * + * When we have finished with a compressed mailbox, we put back the original + * name. + * ctx->path = "mailbox.gz" + * ctx->realpath = NULL + */ +static void +restore_path (CONTEXT *ctx) +{ + if (!ctx) + return; + + FREE(&ctx->path); + ctx->path = ctx->realpath; + ctx->realpath = NULL; +} + +/** + * get_size - Get the size of a file + * @path: File to measure + * + * Returns: + * number: Size in bytes + * 0: On error + */ +static int +get_size (const char *path) +{ + if (!path) + return 0; + + struct stat sb; + if (stat (path, &sb) != 0) + return 0; + + return sb.st_size; +} + +/** + * store_size - Save the size of the compressed file + * @ctx: Mailbox + * + * Save the compressed file size in the compress_info struct. + */ +static void +store_size (const CONTEXT *ctx) +{ + if (!ctx) + return; + + COMPRESS_INFO *ci = ctx->compress_info; + if (!ci) + return; + + ci->size = get_size (ctx->realpath); +} + +/** + * find_hook - Find a hook to match a path + * @type: Type of hook, e.g. MUTT_CLOSEHOOK + * @path: Filename to test + * + * Each hook has a type and a pattern. + * Find a command that matches the type and path supplied. e.g. + * + * User config: + * open-hook '\.gz$' "gzip -cd '%f' > '%t'" + * + * Call: + * find_hook (MUTT_OPENHOOK, "myfile.gz"); + * + * Returns: + * string: Matching hook command + * NULL: No matches + */ +static const char * +find_hook (int type, const char *path) +{ + if (!path) + return NULL; + + const char *c = mutt_find_hook (type, path); + if (!c || !*c) + return NULL; + + return c; +} + +/** + * set_compress_info - Find the compress hooks for a mailbox + * @ctx: Mailbox to examine + * + * When a mailbox is opened, we check if there are any matching hooks. + * + * Note: Caller must free the COMPRESS_INFO when done. + * + * Returns: + * COMPRESS_INFO: Hook info for the mailbox's path + * NULL: On error + */ +static COMPRESS_INFO * +set_compress_info (CONTEXT *ctx) +{ + if (!ctx || !ctx->path) + return NULL; + + if (ctx->compress_info) + return ctx->compress_info; + + /* Open is compulsory */ + const char *o = find_hook (MUTT_OPENHOOK, ctx->path); + if (!o) + return NULL; + + const char *c = find_hook (MUTT_CLOSEHOOK, ctx->path); + const char *a = find_hook (MUTT_APPENDHOOK, ctx->path); + + COMPRESS_INFO *ci = safe_calloc (1, sizeof (COMPRESS_INFO)); + ctx->compress_info = ci; + + ci->open = o; + ci->close = c; + ci->append = a; + + return ci; +} + +/** + * cb_format_str - Expand the filenames in the command string + * @dest: Buffer in which to save string + * @destlen: Buffer length + * @col: Starting column, UNUSED + * @cols: Number of screen columns, UNUSED + * @op: printf-like operator, e.g. 't' + * @src: printf-like format string + * @fmt: Field formatting string, UNUSED + * @ifstring: If condition is met, display this string, UNUSED + * @elsestring: Otherwise, display this string, UNUSED + * @data: Pointer to the mailbox CONTEXT + * @flags: Format flags, UNUSED + * + * cb_format_str is a callback function for mutt_FormatString. It understands + * two operators. '%f' : 'from' filename, '%t' : 'to' filename. + * + * Returns: src (unchanged) + */ +static const char * +cb_format_str (char *dest, size_t destlen, size_t col, int cols, char op, const char *src, + const char *fmt, const char *ifstring, const char *elsestring, + unsigned long data, format_flag flags) +{ + if (!dest || (data == 0)) + return src; + + CONTEXT *ctx = (CONTEXT *) data; + + switch (op) + { + case 'f': + /* Compressed file */ + snprintf (dest, destlen, "%s", ctx->realpath); + break; + case 't': + /* Plaintext, temporary file */ + snprintf (dest, destlen, "%s", ctx->path); + break; + } + return src; +} + +/** + * expand_command_str - Expand placeholders in command string + * @ctx: Mailbox for paths + * @buf: Buffer to store the command + * @buflen: Size of the buffer + * + * This function takes a hook command and expands the filename placeholders + * within it. The function calls mutt_FormatString() to do the replacement + * which calls our callback function cb_format_str(). e.g. + * + * Template command: + * gzip -cd '%f' > '%t' + * + * Result: + * gzip -dc '~/mail/abc.gz' > '/tmp/xyz' + */ +static void +expand_command_str (const CONTEXT *ctx, const char *cmd, char *buf, int buflen) +{ + if (!ctx || !cmd || !buf) + return; + + mutt_FormatString (buf, buflen, 0, buflen, cmd, cb_format_str, (unsigned long) ctx, 0); +} + +/** + * execute_command - Run a system command + * @ctx: Mailbox to work with + * @command: Command string to execute + * @create_file: Should the tmp file be created? + * @progress: Message to show the user + * + * Run the supplied command, taking care of all the Mutt requirements, + * such as locking files and blocking signals. + * + * Returns: + * 1: Success + * 0: Failure + */ +static int +execute_command (CONTEXT *ctx, const char *command, int create_file, const char *progress) +{ + if (!ctx || !command || !progress) + return 0; + + if (!ctx->quiet) + mutt_message (progress, ctx->realpath); + + FILE *fp; + if (create_file) + fp = fopen (ctx->realpath, "a"); + else + fp = fopen (ctx->realpath, "r"); + + if (!fp) + { + mutt_perror (ctx->realpath); + return 0; + } + + mutt_block_signals(); + /* If we're creating the file, lock it exclusively */ + if (!lock_mailbox (ctx, fp, create_file)) + { + safe_fclose (&fp); + mutt_unblock_signals(); + mutt_error (_("Unable to lock mailbox!")); + return 0; + } + + endwin(); + fflush (stdout); + + char sys_cmd[HUGE_STRING]; + + expand_command_str (ctx, command, sys_cmd, sizeof (sys_cmd)); + + int rc = mutt_system (sys_cmd); + if (rc != 0) + { + mutt_any_key_to_continue (NULL); + mutt_error (_("Error executing: %s\n"), sys_cmd); + } + + unlock_mailbox (ctx, fp); + mutt_unblock_signals(); + safe_fclose (&fp); + + return 1; +} + +/** + * open_read - Open a compressed mailbox for reading + * @ctx: Mailbox to open + * + * Decompress the mailbox and set up the paths and hooks needed. + * + * Note: The message handling will be delegated to the mbox code. + * + * Returns: + * 1: Success + * 0: Failure + */ +static int +open_read (CONTEXT *ctx) +{ + if (!ctx) + return 0; + + COMPRESS_INFO *ci = set_compress_info (ctx); + if (!ci) + { + ctx->magic = 0; + return 0; + } + + /* If there's no close-hook, or the file isn't writable */ + if (!ci->close || (access (ctx->path, W_OK) != 0)) + ctx->readonly = 1; + + setup_paths (ctx); + store_size (ctx); + + int rc = execute_command (ctx, ci->open, 0, _("Decompressing %s")); + if (rc == 0) + { + goto or_fail; + } + + ctx->magic = mx_get_magic (ctx->path); + if (ctx->magic == 0) + { + mutt_error (_("Can't identify the contents of the compressed file")); + goto or_fail; + } + + ci->child_ops = mx_get_ops (ctx->magic); + if (!ci->child_ops) + { + mutt_error (_("Can't find mailbox ops for mailbox type %d"), ctx->magic); + goto or_fail; + } + + return 1; + +or_fail: + /* remove the partial uncompressed file */ + remove (ctx->path); + restore_path (ctx); + return 0; +} + + +struct mx_ops mx_comp_ops; + +/** + * open_mailbox - Open a compressed mailbox + * @ctx: Mailbox to open + * + * Set up a compressed mailbox to be read. + * First call open_read() to decompress the file. + * Then determine the type of the mailbox so we can delegate the handling of + * messages. + */ +static int +open_mailbox (CONTEXT *ctx) +{ + if (!ctx || (ctx->magic != MUTT_COMPRESSED)) + return 1; + + if (!open_read (ctx)) + return 1; + + COMPRESS_INFO *ci = ctx->compress_info; + if (!ci) + return 1; + + struct mx_ops *ops = ci->child_ops; + if (!ops) + return 1; + + /* Delegate */ + return ops->open (ctx); +} + +/** + * open_append_mailbox - Open a compressed mailbox for appending + * @ctx: Mailbox to open + * @flags: e.g. Does the file already exist? + * + * To append to a compressed mailbox we need an append-hook (or both open- and + * close-hooks). + * + * Returns: + * 0: Success + * -1: Failure + */ +static int +open_append_mailbox (CONTEXT *ctx, int flags) +{ + if (!ctx) + return -1; + + /* If this succeeds, we know there's an open-hook */ + COMPRESS_INFO *ci = set_compress_info (ctx); + if (!ci) + return -1; + + /* To append we need an append-hook or a close-hook */ + if (!ci->append && !ci->close) + { + FREE(&ctx->compress_info); + mutt_error (_("Cannot append without an append-hook or close-hook : %s"), ctx->path); + return -1; + } + + ctx->magic = DefaultMagic; + /* We can only deal with mbox and mmdf mailboxes */ + if ((ctx->magic != MUTT_MBOX) && (ctx->magic != MUTT_MMDF)) + return -1; + + setup_paths (ctx); + + ctx->mx_ops = &mx_comp_ops; + ci->child_ops = mx_get_ops (ctx->magic); + if (!ci->child_ops) + { + mutt_error (_("Can't find mailbox ops for mailbox type %d"), ctx->magic); + return -1; + } + + if (ci->append) + { + /* Create an empty temporary file */ + ctx->fp = safe_fopen (ctx->path, "w"); + if (!ctx->fp) + { + mutt_perror (ctx->path); + goto oa_fail; + } + } + else + { + /* Open the existing mailbox */ + int rc = execute_command (ctx, ci->open, 0, _("Decompressing %s")); + if (rc == 0) + { + mutt_error (_("Compress command failed: %s"), ci->open); + goto oa_fail; + } + ctx->fp = safe_fopen (ctx->path, "a"); + if (!ctx->fp) + { + mutt_perror (ctx->path); + goto oa_fail; + } + } + + return 0; + +oa_fail: + /* remove the partial uncompressed file */ + remove (ctx->path); + restore_path (ctx); + return -1; +} + +/** + * close_mailbox - Close a compressed mailbox + * @ctx: Mailbox to close + * + * If the mailbox has been changed then re-compress the tmp file. + * Then delete the tmp file. + * + * Returns: + * 0: Success + * -1: Failure + */ +static int +close_mailbox (CONTEXT *ctx) +{ + if (!ctx) + return -1; + + COMPRESS_INFO *ci = ctx->compress_info; + if (!ci) + return -1; + + safe_fclose (&ctx->fp); + + /* sync has already been called, so we only need to delete some files */ + if (!ctx->append) + { + /* If the file was removed, remove the compressed folder too */ + if ((access (ctx->path, F_OK) != 0) && !option (OPTSAVEEMPTY)) + { + remove (ctx->realpath); + } + else + { + remove (ctx->path); + } + + restore_path (ctx); + FREE(&ctx->compress_info); + return 0; + } + + const char *append; + const char *msg; + + /* The file exists and we can append */ + if ((access (ctx->realpath, F_OK) == 0) && ci->append) + { + append = ci->append; + msg = _("Compressed-appending to %s..."); + } + else + { + append = ci->close; + msg = _("Compressing %s..."); + } + + int rc = execute_command (ctx, append, 1, msg); + if (rc == 0) + { + mutt_any_key_to_continue (NULL); + mutt_error (_(" %s: Error compressing mailbox! Uncompressed one kept!\n"), ctx->path); + } + + remove (ctx->path); + restore_path (ctx); + FREE(&ctx->compress_info); + + return 0; +} + +/** + * check_mailbox - Check for changes in the compressed file + * @ctx: Mailbox + * + * If the compressed file changes in size but the mailbox hasn't been changed + * in Mutt, then we can close and reopen the mailbox. + * + * If the mailbox has been changed in Mutt, warn the user. + * + * The return codes are picked to match mx_check_mailbox(). + * + * Returns: + * 0: Mailbox OK + * MUTT_REOPENED: The mailbox was closed and reopened + * -1: Mailbox bad + */ +static int +check_mailbox (CONTEXT *ctx, int *index_hint) +{ + if (!ctx) + return -1; + + COMPRESS_INFO *ci = ctx->compress_info; + if (!ci) + return -1; + + int size = get_size (ctx->realpath); + if (size == ci->size) + return 0; + + if (ctx->changed) + { + FREE(&ctx->compress_info); + restore_path (ctx); + mutt_error (_("Mailbox was corrupted!")); + return -1; + } + + close_mailbox (ctx); + + const char *path = ctx->path; + ctx->path = NULL; + + mx_open_mailbox (path, 0, ctx); + FREE(&path); + + return MUTT_REOPENED; +} + + +/** + * open_message - Delegated to mbox handler + */ +static int +open_message (CONTEXT *ctx, MESSAGE *msg, int msgno) +{ + if (!ctx) + return -1; + + COMPRESS_INFO *ci = ctx->compress_info; + if (!ci) + return -1; + + struct mx_ops *ops = ci->child_ops; + if (!ops) + return -1; + + /* Delegate */ + return ops->open_msg (ctx, msg, msgno); +} + +/** + * close_message - Delegated to mbox handler + */ +static int +close_message (CONTEXT *ctx, MESSAGE *msg) +{ + if (!ctx) + return -1; + + COMPRESS_INFO *ci = ctx->compress_info; + if (!ci) + return -1; + + struct mx_ops *ops = ci->child_ops; + if (!ops) + return -1; + + /* Delegate */ + return ops->close_msg (ctx, msg); +} + +/** + * commit_message - Delegated to mbox handler + */ +static int +commit_message (CONTEXT *ctx, MESSAGE *msg) +{ + if (!ctx) + return -1; + + COMPRESS_INFO *ci = ctx->compress_info; + if (!ci) + return -1; + + struct mx_ops *ops = ci->child_ops; + if (!ops) + return -1; + + /* Delegate */ + return ops->commit_msg (ctx, msg); +} + +/** + * open_new_message - Delegated to mbox handler + */ +static int +open_new_message (MESSAGE *msg, CONTEXT *ctx, HEADER *hdr) +{ + if (!ctx) + return -1; + + COMPRESS_INFO *ci = ctx->compress_info; + if (!ci) + return -1; + + struct mx_ops *ops = ci->child_ops; + if (!ops) + return -1; + + /* Delegate */ + return ops->open_new_msg (msg, ctx, hdr); +} + + +/** + * comp_can_append - Can we append to this path? + * @path: pathname of file to be tested + * + * To append to a file we can either use an 'append-hook' or a combination of + * 'open-hook' and 'close-hook'. + * + * A match means it's our responsibility to append to the file. + * + * Returns: + * 1: Yes, we can append to the file + * 0: No, appending isn't possible + */ +int +comp_can_append (CONTEXT *ctx) +{ + if (!ctx) + return 0; + + /* If this succeeds, we know there's an open-hook */ + COMPRESS_INFO *ci = set_compress_info (ctx); + if (!ci) + return 0; + + /* We have an open-hook, so to append we need an append-hook, + * or a close-hook. */ + if (ci->append || ci->close) + return 1; + + mutt_error (_("Cannot append without an append-hook or close-hook : %s"), ctx->path); + return 0; +} + +/** + * comp_can_read - Can we read from this file? + * @path: Pathname of file to be tested + * + * Search for an 'open-hook' with a regex that matches the path. + * + * A match means it's our responsibility to open the file. + * + * Returns: + * 1: Yes, we can read the file + * 0: No, we cannot read the file + */ +int +comp_can_read (const char *path) +{ + if (!path) + return 0; + + if (find_hook (MUTT_OPENHOOK, path)) + return 1; + else + return 0; +} + +/** + * comp_sync - Save changes to the compressed mailbox file + * @ctx: Mailbox to sync + * + * Changes in Mutt only affect the tmp file. Calling comp_sync() will commit + * them to the compressed file. + * + * Returns: + * 0: Success + * -1: Failure + */ +int +comp_sync (CONTEXT *ctx) +{ + if (!ctx) + return -1; + + COMPRESS_INFO *ci = ctx->compress_info; + if (!ci) + return -1; + + if (!ci->close) + { + mutt_error (_("Can't sync a compressed file without a close-hook")); + return -1; + } + + int rc = execute_command (ctx, ci->close, 1, _("Compressing %s")); + if (rc == 0) + return -1; + + store_size (ctx); + + return 0; +} + +/** + * comp_valid_command - Is this command string allowed? + * @cmd: Command string + * + * A valid command string must have both "%f" (from file) and "%t" (to file). + * We don't check if we can actually run the command. + * + * Returns: + * 1: Valid command + * 0: "%f" and/or "%t" is missing + */ +int +comp_valid_command (const char *cmd) +{ + if (!cmd) + return 0; + + return (strstr (cmd, "%f") && strstr (cmd, "%t")); +} + + +/** + * mx_comp_ops - Mailbox callback functions + * + * Compress only uses open, close and check. + * The message functions are delegated to mbox. + */ +struct mx_ops mx_comp_ops = +{ + .open = open_mailbox, + .open_append = open_append_mailbox, + .close = close_mailbox, + .check = check_mailbox, + .open_msg = open_message, + .close_msg = close_message, + .commit_msg = commit_message, + .open_new_msg = open_new_message +}; + diff --git a/compress.h b/compress.h new file mode 100644 index 00000000..26beae63 --- /dev/null +++ b/compress.h @@ -0,0 +1,29 @@ +/* Copyright (C) 1997 Alain Penders + * Copyright (C) 2016 Richard Russon + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +#ifndef _COMPRESS_H_ +#define _COMPRESS_H_ + +int comp_can_append (CONTEXT *ctx); +int comp_can_read (const char *path); +int comp_sync (CONTEXT *ctx); +int comp_valid_command (const char *cmd); + +extern struct mx_ops mx_comp_ops; + +#endif /* _COMPRESS_H_ */ diff --git a/configure.ac b/configure.ac index 6d1c418a..57e408e4 100644 --- a/configure.ac +++ b/configure.ac @@ -183,6 +183,15 @@ AC_ARG_ENABLE(sidebar, AC_HELP_STRING([--enable-sidebar], [Enable Sidebar suppor fi ]) +AC_ARG_ENABLE(compressed, AC_HELP_STRING([--enable-compressed], [Enable compressed folders support]), + enable_compressed=$enableval, enable_compressed=no +) +AS_IF([test x$enable_compressed = "xyes"], [ + AC_DEFINE(USE_COMPRESSED, 1, [Define to enable compressed folders support.]) + MUTT_LIB_OBJECTS="$MUTT_LIB_OBJECTS compress.o" +]) +AM_CONDITIONAL(BUILD_COMPRESS, test x$enable_compressed = xyes) + AC_ARG_WITH(mixmaster, AS_HELP_STRING([--with-mixmaster@<:@=PATH@:>@],[Include Mixmaster support]), [if test "$withval" != no then diff --git a/contrib/Makefile.am b/contrib/Makefile.am index 3ca99a02..89aa1586 100644 --- a/contrib/Makefile.am +++ b/contrib/Makefile.am @@ -4,7 +4,7 @@ subdir = contrib SAMPLES = Mush.rc Pine.rc gpg.rc pgp2.rc pgp5.rc pgp6.rc Tin.rc \ sample.mailcap sample.muttrc sample.muttrc-sidebar sample.muttrc-tlr \ - sample.vimrc-sidebar colors.default colors.linux smime.rc \ + sample.muttrc-compress sample.vimrc-sidebar colors.default colors.linux smime.rc \ ca-bundle.crt smime_keys_test.pl mutt_xtitle EXTRA_DIST = language.txt language50.txt \ diff --git a/contrib/sample.muttrc-compress b/contrib/sample.muttrc-compress new file mode 100644 index 00000000..d793a9e1 --- /dev/null +++ b/contrib/sample.muttrc-compress @@ -0,0 +1,38 @@ +# Example Mutt config file for the compress feature. + +# This feature adds three hooks to Mutt which allow it to +# work with compressed, or encrypted, mailboxes. + +# The hooks are of the form: +# open-hook regexp "shell-command" +# close-hook regexp "shell-command" +# append-hook regexp "shell-command" + +# The 'append-hook' is optional. + +# Handler for gzip compressed mailboxes +open-hook '\.gz$' "gzip -cd '%f' > '%t'" +close-hook '\.gz$' "gzip -c '%t' > '%f'" +append-hook '\.gz$' "gzip -c '%t' >> '%f'" + +# Handler for bzip2 compressed mailboxes +open-hook '\.bz2$' "bzip2 -cd '%f' > '%t'" +close-hook '\.bz2$' "bzip2 -c '%t' > '%f'" +append-hook '\.bz2$' "bzip2 -c '%t' >> '%f'" + +# Handler for xz compressed mailboxes +open-hook '\.xz$' "xz -cd '%f' > '%t'" +close-hook '\.xz$' "xz -c '%t' > '%f'" +append-hook '\.xz$' "xz -c '%t' >> '%f'" + +# Handler for pgp encrypted mailboxes +# PGP does not support appending to an encrypted file +open-hook '\.pgp$' "pgp -f < '%f' > '%t'" +close-hook '\.pgp$' "pgp -fe YourPgpUserIdOrKeyId < '%t' > '%f'" + +# Handler for gpg encrypted mailboxes +# gpg does not support appending to an encrypted file +open-hook '\.gpg$' "gpg --decrypt < '%f' > '%t'" +close-hook '\.gpg$' "gpg --encrypt --recipient YourGpgUserIdOrKeyId < '%t' > '%f'" + +# vim: syntax=muttrc diff --git a/curs_main.c b/curs_main.c index 3793eccc..03e2f6a2 100644 --- a/curs_main.c +++ b/curs_main.c @@ -1241,6 +1241,11 @@ int mutt_index_menu (void) { int check; +#ifdef USE_COMPRESSED + if (Context->compress_info && Context->realpath) + mutt_str_replace (&LastFolder, Context->realpath); + else +#endif mutt_str_replace (&LastFolder, Context->path); oldcount = Context ? Context->msgcount : 0; diff --git a/doc/Muttrc.head b/doc/Muttrc.head index 1f7012e7..78916d95 100644 --- a/doc/Muttrc.head +++ b/doc/Muttrc.head @@ -29,6 +29,11 @@ macro generic,pager " less @docdir@/manual.txt" "show macro index,pager y "?" "show incoming mailboxes list" bind browser y exit +# Handler for gzip compressed mailboxes +# open-hook '\.gz$' "gzip -cd '%f' > '%t'" +# close-hook '\.gz$' "gzip -c '%t' > '%f'" +# append-hook '\.gz$' "gzip -c '%t' >> '%f'" + # If Mutt is unable to determine your site's domain name correctly, you can # set the default here. # diff --git a/doc/manual.xml.head b/doc/manual.xml.head index db88f87e..af8ba014 100644 --- a/doc/manual.xml.head +++ b/doc/manual.xml.head @@ -8427,6 +8427,282 @@ please have a look at the mixmaster documentation. + + Compressed Folders Feature + Read from/write to compressed mailboxes + + + Introduction + + + The Compressed Folder patch allows Mutt to read mailbox files that are + compressed. But it isn't limited to compressed files. It works well + with encrypted files, too. In fact, if you can create a program/script + to convert to and from your format, then Mutt can read it. + + + + The patch adds three hooks to Mutt: open-hook, + close-hook and append-hook. They + define commands to: uncompress a file; compress a file; append + messages to an already compressed file. + + + + There are some examples of both compressed and encrypted files, + later. For now, the documentation will just concentrate on + compressed files. + + + + + + Commands + + open-hook + + pattern + + + shell-command + + close-hook + + pattern + + + shell-command + + append-hook + + pattern + + + shell-command + + + + + The shell-command must contain two placeholders for filenames: + %f and %t. These represent + from and to filenames. It's a good idea to + put quotes around these placeholders. + + + + If you need the exact string %f or %t in your + command, simply double up the % character, e.g. + %%f or %%t. + + + + Not all Hooks are Required + + + + Open + Close + Append + Effect + Useful if + + + + + Open + - + - + Folder is readonly + The folder is just a backup + + + Open + Close + - + Folder is read/write, but the entire folder must be + written if anything is changed + Your compression format doesn't support appending + + + Open + Close + Append + Folder is read/write and emails can be efficiently added + to the end + Your compression format supports appending + + + Open + - + Append + Folder is readonly, but can be appended to + You want to store emails, but never change them + + + +
+ + + The command: + + should return a non-zero exit status on failure + should not delete any files + + + + + Read from compressed mailbox + + open-hook regexp shell-command + + + If Mutt is unable to open a file, it then looks for + open-hook that matches the filename. + + + + If your compression program doesn't have a well-defined extension, + then you can use . as the regexp. + + + + Example of open-hook + + open-hook '\.gz$' "gzip -cd '%f' > '%t'" + + + Mutt finds a file, example.gz, + that it can't read + Mutt has an open-hook + whose regexp matches the filename: + \.gz$ + Mutt uses the command gzip -cd + to create a temporary file that it can + read + + + + + + Write to a compressed mailbox + + close-hook regexp shell-command + + + When Mutt has finished with a compressed mail folder, it will look + for a matching close-hook to recompress the file. + This hook is optional. + + + + + If the folder has not been modified, the + close-hook will not be called. + + + + + Example of close-hook + + close-hook '\.gz$' "gzip -c '%t' > '%f'" + + + Mutt has finished with a folder, example.gz, + that it opened with open-hook + The folder has been modified + Mutt has a close-hook whose regexp + matches the filename: \.gz$ + Mutt uses the command gzip -c + to create a new compressed file + + + + + + Append to a compressed mailbox + + append-hook regexp shell-command + + + When Mutt wants to append an email to a compressed mail folder, it + will look for a matching append-hook. + This hook is optional. + + + + Using the append-hook will save time, but + Mutt won't be able to determine the type of the mail folder + inside the compressed file. + + + + Mutt will assume the type to be that of + the $mbox_type variable. Mutt also uses + this type for temporary files. + + + + Mutt will only use the append-hook for existing files. + The close-hook will be used for empty, or missing files. + + + + + If your command writes to stdout, it is vital that you use + >> in the append-hook. + If not, data will be lost. + + + + + Example of append-hook + + append-hook '\.gz$' "gzip -c '%t' >> '%f'" + + + Mutt wants to append an email to a folder, example.gz, + that it opened with open-hook + Mutt has an append-hook whose regexp matches + the filename: \.gz$ + Mutt knows the mailbox type from the $mbox + variable + Mutt uses the command gzip -c + to append to an existing compressed file + + + + + + + Empty Files + + + Mutt assumes that an empty file is not compressed. In this + situation, unset $save_empty, so + that the compressed file will be removed if you delete all of the + messages. + + + + + Security + + + Encrypted files are decrypted into temporary files which are + stored in the $tmpdir directory. + This could be a security risk. + + +
+ + + Known Bugs + + + The Compressed Folder hooks cannot deal with filenames that contains quotes/apostrophes. + + +
diff --git a/doc/muttrc.man.head b/doc/muttrc.man.head index b237c5a2..6ab69a70 100644 --- a/doc/muttrc.man.head +++ b/doc/muttrc.man.head @@ -358,7 +358,24 @@ You may use multiple \fBcrypt-hook\fPs with the same \fIregexp\fP; multiple matching \fBcrypt-hook\fPs result in the use of multiple \fIkey-id\fPs for a recipient. - +.PP +.nf +\fBopen-hook\fP \fIregexp\fP "\fIcommand\fP" +\fBclose-hook\fP \fIregexp\fP "\fIcommand\fP" +\fBappend-hook\fP \fIregexp\fP "\fIcommand\fP" +.fi +.IP +These commands provide a way to handle compressed folders. The given +\fBregexp\fP specifies which folders are taken as compressed (e.g. +"\fI\\\\.gz$\fP"). The commands tell Mutt how to uncompress a folder +(\fBopen-hook\fP), compress a folder (\fBclose-hook\fP) or append a +compressed mail to a compressed folder (\fBappend-hook\fP). The +\fIcommand\fP string is the +.BR printf (3) +like format string, and it should accept two parameters: \fB%f\fP, +which is replaced with the (compressed) folder name, and \fB%t\fP +which is replaced with the name of the temporary folder to which to +write. .TP \fBpush\fP \fIstring\fP This command adds the named \fIstring\fP to the keyboard buffer. diff --git a/hook.c b/hook.c index 1b906c33..d0a111b2 100644 --- a/hook.c +++ b/hook.c @@ -24,6 +24,10 @@ #include "mailbox.h" #include "mutt_crypt.h" +#ifdef USE_COMPRESSED +#include "compress.h" +#endif + #include #include #include @@ -109,6 +113,14 @@ int mutt_parse_hook (BUFFER *buf, BUFFER *s, unsigned long data, BUFFER *err) memset (&pattern, 0, sizeof (pattern)); pattern.data = safe_strdup (path); } +#ifdef USE_COMPRESSED + else if (data & (MUTT_APPENDHOOK | MUTT_OPENHOOK | MUTT_CLOSEHOOK)) { + if (comp_valid_command (command.data) == 0) { + strfcpy (err->data, _("badly formatted command string"), err->dsize); + return -1; + } + } +#endif else if (DefaultHook && !(data & (MUTT_CHARSETHOOK | MUTT_ICONVHOOK | MUTT_ACCOUNTHOOK)) && (!WithCrypto || !(data & MUTT_CRYPTHOOK)) ) diff --git a/init.h b/init.h index 48cf915a..a0a399c1 100644 --- a/init.h +++ b/init.h @@ -3907,6 +3907,11 @@ const struct command_t Commands[] = { { "fcc-hook", mutt_parse_hook, MUTT_FCCHOOK }, { "fcc-save-hook", mutt_parse_hook, MUTT_FCCHOOK | MUTT_SAVEHOOK }, { "folder-hook", mutt_parse_hook, MUTT_FOLDERHOOK }, +#ifdef USE_COMPRESSED + { "open-hook", mutt_parse_hook, MUTT_OPENHOOK }, + { "close-hook", mutt_parse_hook, MUTT_CLOSEHOOK }, + { "append-hook", mutt_parse_hook, MUTT_APPENDHOOK }, +#endif { "group", parse_group, MUTT_GROUP }, { "ungroup", parse_group, MUTT_UNGROUP }, { "hdr_order", parse_list, UL &HeaderOrderList }, diff --git a/main.c b/main.c index 68a36018..25b1d04b 100644 --- a/main.c +++ b/main.c @@ -494,6 +494,12 @@ static void show_version (void) "-USE_SIDEBAR " #endif +#ifdef USE_COMPRESSED + "+USE_COMPRESSED " +#else + "-USE_COMPRESSED " +#endif + ); #ifdef ISPELL diff --git a/mutt.h b/mutt.h index 95c5f0e4..c63a1eeb 100644 --- a/mutt.h +++ b/mutt.h @@ -146,6 +146,11 @@ typedef enum #define MUTT_ACCOUNTHOOK (1<<9) #define MUTT_REPLYHOOK (1<<10) #define MUTT_SEND2HOOK (1<<11) +#ifdef USE_COMPRESSED +#define MUTT_OPENHOOK (1<<12) +#define MUTT_APPENDHOOK (1<<13) +#define MUTT_CLOSEHOOK (1<<14) +#endif /* tree characters for linearize_tree and print_enriched_string */ #define MUTT_TREE_LLCORNER 1 @@ -951,6 +956,10 @@ typedef struct _context unsigned int closing : 1; /* mailbox is being closed */ unsigned int peekonly : 1; /* just taking a glance, revert atime */ +#ifdef USE_COMPRESSED + void *compress_info; /* compressed mbox module private data */ +#endif /* USE_COMPRESSED */ + /* driver hooks */ void *data; /* driver specific data */ struct mx_ops *mx_ops; diff --git a/mx.c b/mx.c index 680d38f4..fdb2a77c 100644 --- a/mx.c +++ b/mx.c @@ -33,6 +33,10 @@ #include "sidebar.h" #endif +#ifdef USE_COMPRESSED +#include "compress.h" +#endif + #ifdef USE_IMAP #include "imap.h" #endif @@ -60,7 +64,7 @@ #include #include -static struct mx_ops* mx_get_ops (int magic) +struct mx_ops* mx_get_ops (int magic) { switch (magic) { @@ -79,6 +83,10 @@ static struct mx_ops* mx_get_ops (int magic) #ifdef USE_POP case MUTT_POP: return &mx_pop_ops; +#endif +#ifdef USE_COMPRESSED + case MUTT_COMPRESSED: + return &mx_comp_ops; #endif default: return NULL; @@ -441,6 +449,12 @@ int mx_get_magic (const char *path) return (-1); } +#ifdef USE_COMPRESSED + /* If there are no other matches, see if there are any + * compress hooks that match */ + if ((magic == 0) && comp_can_read (path)) + return MUTT_COMPRESSED; +#endif return (magic); } @@ -507,6 +521,11 @@ static int mx_open_mailbox_append (CONTEXT *ctx, int flags) return -1; } +#ifdef USE_COMPRESSED + if (comp_can_append (ctx)) + ctx->mx_ops = &mx_comp_ops; + else +#endif ctx->mx_ops = mx_get_ops (ctx->magic); if (!ctx->mx_ops || !ctx->mx_ops->open_append) return -1; @@ -700,6 +719,14 @@ static int sync_mailbox (CONTEXT *ctx, int *index_hint) if (tmp && tmp->new == 0) mutt_update_mailbox (tmp); + +#ifdef USE_COMPRESSED + /* If everything went well, the mbox handler saved the changes to our + * temporary file. Next, comp_sync() will compress the temporary file. */ + if ((rc == 0) && ctx->compress_info) + return comp_sync (ctx); +#endif + return rc; } diff --git a/mx.h b/mx.h index ba9a78f9..6b3c535e 100644 --- a/mx.h +++ b/mx.h @@ -37,6 +37,9 @@ enum MUTT_MAILDIR, MUTT_IMAP, MUTT_POP +#ifdef USE_COMPRESSED + , MUTT_COMPRESSED +#endif }; WHERE short DefaultMagic INITVAL (MUTT_MBOX); @@ -70,6 +73,7 @@ void mx_update_tables (CONTEXT *, int); int mx_lock_file (const char *, int, int, int, int); int mx_unlock_file (const char *path, int fd, int dot); +struct mx_ops* mx_get_ops (int magic); extern struct mx_ops mx_maildir_ops; extern struct mx_ops mx_mbox_ops; extern struct mx_ops mx_mh_ops; diff --git a/po/POTFILES.in b/po/POTFILES.in index 2d01add6..3654ad14 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -8,6 +8,7 @@ charset.c color.c commands.c compose.c +compress.c crypt-gpgme.c crypt.c cryptglue.c diff --git a/po/de.po b/po/de.po index 19ddc349..bb213183 100644 --- a/po/de.po +++ b/po/de.po @@ -5418,6 +5418,31 @@ msgstr "Extrahiere unterst msgid "show S/MIME options" msgstr "Zeige S/MIME Optionen" +#: compress.c:228 compress.c:253 +#, c-format +msgid "Decompressing %s" +msgstr "Entpacke %s" + +#: compress.c:264 +#, c-format +msgid "Error executing: %s\n" +msgstr "Fehler beim Ausführen von %s\n" + +#: compress.c:350 compress.c:377 compress.c:423 compress.c:454 +#, c-format +msgid "Compressing %s..." +msgstr "Komprimiere %s..." + +#: compress.c:425 compress.c:456 +#, c-format +msgid "Compressed-appending to %s..." +msgstr "Hänge komprimiert an %s... an" + +#: compress.c:461 +#, c-format +msgid " %s: Error compressing mailbox! Uncompressed one kept!\n" +msgstr " %s: Fehler beim packen der Mailbox! Entpackte Mailbox gespeichert!\n" + #~ msgid "delete message(s)" #~ msgstr "Nachricht(en) löschen" diff --git a/status.c b/status.c index 6ac5b56b..86113a8e 100644 --- a/status.c +++ b/status.c @@ -96,6 +96,12 @@ status_format_str (char *buf, size_t buflen, size_t col, int cols, char op, cons case 'f': snprintf (fmt, sizeof(fmt), "%%%ss", prefix); +#ifdef USE_COMPRESSED + if (Context && Context->compress_info && Context->realpath) { + strfcpy (tmp, Context->realpath, sizeof (tmp)); + mutt_pretty_mailbox (tmp, sizeof (tmp)); + } else +#endif if (Context && Context->path) { strfcpy (tmp, Context->path, sizeof (tmp)); -- 2.40.0