Skip to content

Instantly share code, notes, and snippets.

@skx
Last active August 29, 2015 14:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save skx/ba07ba7fbb0788c6ba68 to your computer and use it in GitHub Desktop.
Save skx/ba07ba7fbb0788c6ba68 to your computer and use it in GitHub Desktop.
Simple utility script to dump attachments, via GMIME
/**
* AD.c - Attachment dumper.
***
*
* This is a simple script which is designed to write out the attachments
* from an email to disk.
*
* Compile like so:
*
* $ gcc -Wall -O2 ad.c -o ad $(pkg-config --libs --cflags gmime-2.6)
*
* Specify the path to a (Maildir) message on the command-line and attachments
* will be written to the current working directory.
*
* For example:
*
* $ ./ad ~/Mail/.razor/cur/1410371138.30015_2.ssh.steve.org.uk\:2\,S --mime image/jpeg
*
* Here we've used "--mime" to specify that only attachments with a
* Content-Type: image/jpeg-header will be dumped.
*
* You may also invoke the script with `--dump` to just show the attachments:
*
* Parsing: ./1410371138.30015_2.ssh.steve.org.uk:2,S
* Attachment name: picture1.jpg
* MIME-Type: application/octet-stream
* Attachment name: picture2.jpg
* MIME-Type: application/octet-stream
* Attachment name: picture3.jpg
* MIME-Type: application/octet-stream
* Attachment name: picture4.jpg
* MIME-Type: application/octet-stream
* Attachment name: picture5.jpg
* MIME-Type: application/octet-stream
* There were 6 attachments
*
* NOTE: --dump will stop the writing from happening.
*
*
* Options
* -------
*
* --dump - Only show attachments
* --verbose - Some more details.
* --name str - Write attachments whos filename matches the string.
* --output /dir - Write attachments to given directory.
* --mime type - Only write out matching parts.
*
*
* CAVEATS
* -------
*
* We don't filter attachment names. If you have an attachment with an
* absolute-name, such as "/etc/passwd" bad things will happen, if you're
* crazy enough to run this as root.
*
*
* SCRIPTING
* ---------
*
* $ find ~/Maildir/.people.kirsi/ -type f -exec $PWD/ad --mime=application/pdf \{\} \;
*
* Fun.
*
*
* Steve
* --
*
*/
#include <glib.h>
#include <gmime/gmime.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <getopt.h>
/**
* Global options.
*/
static int g_debug = 0;
static int g_verbose = 0;
/* Match on MIME-type. */
static char *g_mime = NULL;
/* Match on attachment filename. */
static char *g_file_name = NULL;
/* where to write, defaults to "./" */
static char *g_output_directory = NULL;
/**
* Parse the given message.
*/
static GMimeMessage *parse_message(int fd)
{
GMimeMessage *message;
GMimeParser *parser;
GMimeStream *stream;
stream = g_mime_stream_fs_new(fd);
parser = g_mime_parser_new_with_stream(stream);
/*
* unref the stream (parser owns a ref, so this object does not
* actually get free'd until we destroy the parser)
**/
g_object_unref(stream);
message = g_mime_parser_construct_message(parser);
if (!message)
return NULL;
g_object_unref(parser);
return message;
}
/**
* This function is invoked for each "part" of the message.
*
* The part might be an attachment, another message, or something else ..
*/
static void message_part_callback(GMimeObject * parent, GMimeObject * part, gpointer user_data)
{
int *count = (int *) user_data;
if (GMIME_IS_MESSAGE_PART(part))
{
/* message/rfc822 or message/news */
GMimeMessage *message;
/* Recurse */
message = g_mime_message_part_get_message((GMimeMessagePart *) part);
g_mime_message_foreach(message, message_part_callback, &count);
} else if (GMIME_IS_MESSAGE_PARTIAL(part))
{
/* message/partial */
} else if (GMIME_IS_MULTIPART(part))
{
/* multipart/mixed, multipart/alternative,
* multipart/related, multipart/signed,
* multipart/encrypted, etc... */
} else if (GMIME_IS_PART(part))
{
(*count)++;
GMimeContentType *ct = g_mime_object_get_content_type(part);
/* a normal leaf part, could be text/plain or image/jpeg etc */
GMimeContentDisposition *disp = NULL;
disp = g_mime_object_get_content_disposition(part);
if ((disp != NULL) &&
(!g_ascii_strcasecmp(disp->disposition, "attachment")))
{
char *aname = (char *) g_mime_object_get_content_disposition_parameter(part,
"filename");
if (aname == NULL)
aname = (char *) g_mime_object_get_content_type_parameter(part, "name");
if (aname == NULL)
return;
/**
* Skip any MIME types that don't match.
*/
if (g_mime)
{
char buf[128] = { '\0' };
snprintf(buf, sizeof(buf) - 1, "%s/%s", ct->type, ct->subtype);
if (strcasestr(buf, g_mime) == NULL)
{
if (g_verbose)
{
printf("Skipping attachment of type '%s'\n", buf);
}
return;
}
}
/**
* Skip any filenames that don't match.
*/
if (g_file_name)
{
if (strcasestr(aname, g_file_name) == NULL)
{
if (g_verbose)
{
printf("Skipping attachment name which doesn't match '%s'\n", aname);
}
return;
}
}
/**
* If dumping then just show the data and return.
*/
if (g_debug)
{
printf("\tAttachment name: %s\n", aname);
printf("\tMIME-Type: %s/%s\n", ct->type, ct->subtype);
return;
}
/**
* Get the attachment data.
*/
GMimeStream *mem = g_mime_stream_mem_new();
if (GMIME_IS_MESSAGE_PART(part))
{
GMimeMessage *msg = g_mime_message_part_get_message(GMIME_MESSAGE_PART(part));
g_mime_object_write_to_stream(GMIME_OBJECT(msg), mem);
} else
{
GMimeDataWrapper *content = g_mime_part_get_content_object(GMIME_PART(part));
g_mime_data_wrapper_write_to_stream(content, mem);
}
/**
* NOTE: by setting the owner to FALSE, it means unreffing the
* memory stream won't free the GByteArray data.
*/
g_mime_stream_mem_set_owner(GMIME_STREAM_MEM(mem), FALSE);
GByteArray *res = g_mime_stream_mem_get_byte_array(GMIME_STREAM_MEM(mem));
/**
* The actual data from the array, and the size of that data.
*/
char *adata = (char *) res->data;
size_t len = (res->len);
if ((adata != NULL) && (len > 0))
{
/**
* Build up output directory, as specified via --output
*/
char out[1024] = { '\0' };
if (g_output_directory)
snprintf(out, sizeof(out) - 1, "%s/%s", g_output_directory, aname);
else
snprintf(out, sizeof(out) - 1, "%s", aname);
FILE *fout = fopen(out, "wbx");
if (fout)
{
fwrite(adata, len, 1, fout);
fclose(fout);
} else
{
if (errno == EEXIST)
{
printf("Refused to overwite existing file: %s\n", out);
return;
}
}
if (g_verbose)
printf("Wrote %d bytes to %s\n", (int) len, aname);
}
g_object_unref(mem);
}
} else
{
/** Unknown part-type. */
g_assert_not_reached();
}
}
/**
* Entry point.
*/
int main(int argc, char **argv)
{
/**
* Parse our options.
*/
while (1)
{
static struct option long_options[] = {
{"dump", no_argument, 0, 'd'},
{"output", required_argument, 0, 'o'},
{"name", required_argument, 0, 'n'},
{"mime", required_argument, 0, 'm'},
{"verbose", no_argument, 0, 'v'},
{0, 0, 0, 0}
};
char c;
/* getopt_long stores the option index here. */
int option_index = 0;
c = getopt_long(argc, argv, "vdm:n:o:", long_options, &option_index);
/* Detect the end of the options. */
if (c == -1)
break;
switch (c)
{
case 'd':
g_debug = 1;
break;
case 'v':
g_verbose = 1;
break;
case 'm':
g_mime = strdup(optarg);
break;
case 'n':
g_file_name = strdup(optarg);
break;
case 'o':
g_output_directory = strdup(optarg);
break;
case '?':
/* getopt_long already printed an error message. */
return (1);
}
}
if (optind < 1)
{
fprintf(stderr, "Cannot read from stdin\n");
return -1;
}
/* init the gmime library */
g_mime_init(0);
/**
* If a configuration file was specified use a different one.
*/
int index;
for (index = optind; index < argc; index++)
{
/* parse the message */
int fd;
if ((fd = open(argv[index], O_RDONLY, 0)) == -1)
{
fprintf(stderr, "Cannot open message `%s': %s\n", argv[1], g_strerror(errno));
return 0;
}
if (g_debug)
printf("Parsing: %s\n", argv[index]);
GMimeMessage *message = parse_message(fd);
if (message)
{
/* Invoke our callback */
int count = 0;
g_mime_message_foreach(message, message_part_callback, &count);
if (g_debug)
printf("\tThere were %d attachments\n\n", count);
/* free the mesage */
g_object_unref(message);
}
}
return 0;
}
@skx
Copy link
Author

skx commented Sep 24, 2014

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment