/* Copyright (C) 2020 June McEnroe * * 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 3 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, see . * * Additional permission under GNU GPL version 3 section 7: * * If you modify this Program, or any covered work, by linking or * combining it with OpenSSL (or a modified version of that library), * containing parts covered by the terms of the OpenSSL License and the * original SSLeay license, the licensors of this Program grant you * additional permission to convey the resulting work. Corresponding * Source for a non-source form of such a combination shall include the * source code for the parts of OpenSSL used as well as that of the * covered work. */ #include #include #include #include #include #include #include #include #include #include #include "archive.h" #include "imap.h" static char *exportPath(uint32_t uid, const char *type) { struct Variable vars[] = { { "uid", u32(uid).s }, { "type", type }, {0}, }; return templateString(PATH_UID, vars, escapePath); } static int numberCompare(const void *_a, const void *_b) { const struct Data *a = _a; const struct Data *b = _b; return (a->number > b->number) - (a->number < b->number); } bool exportFetch(FILE *imap, enum Atom tag, struct List threads) { struct List uids = {0}; listFlatten(&uids, threads); for (size_t i = uids.len - 1; i < uids.len; --i) { uint32_t uid = dataCheck(uids.ptr[i], Number).number; char *atom = exportPath(uid, "atom"); char *html = exportPath(uid, "html"); char *mbox = exportPath(uid, "mbox"); int error = 0 || access(atom, F_OK) || access(html, F_OK) || access(mbox, F_OK); if (!error) uids.ptr[i] = uids.ptr[--uids.len]; free(atom); free(html); free(mbox); } if (!uids.len) { listFree(uids); return false; } qsort(uids.ptr, uids.len, sizeof(*uids.ptr), numberCompare); fprintf(imap, "%s UID FETCH ", Atoms[tag]); compressUIDs(imap, uids); listFree(uids); fprintf( imap, " (UID ENVELOPE BODYSTRUCTURE" " BODY[HEADER.FIELDS (" MBOX_HEADERS ")] BODY[TEXT])\r\n" ); return true; } static void exportMbox( uint32_t uid, const struct Envelope *envelope, const char *header, const char *body ) { char *path = exportPath(uid, "mbox"); FILE *file = fopen(path, "w"); if (!file) err(EX_CANTCREAT, "%s", path); int error = 0 || mboxFrom(file) || mboxHeader(file, header) || mboxBody(file, body) || fclose(file); if (error) err(EX_IOERR, "%s", path); struct Variable vars[] = { { "messageID", envelope->messageID }, { "type", "mbox" }, {0}, }; char *dest = templateString(PATH_MESSAGE, vars, escapePath); unlink(dest); error = link(path, dest); if (error) err(EX_CANTCREAT, "%s", dest); if (!quiet) printf("%s\n", dest); free(dest); free(path); } static bool isInline(const struct BodyPart *part) { if (!bodyPartType(part, "text", "plain")) return false; if (!part->disposition.type) return true; return !strcasecmp(part->disposition.type, "inline"); } static bool isAttachment(const struct BodyPart *part) { if (isInline(part)) return false; return !part->multipart && !part->message.structure; } static void exportAtom( uint32_t uid, const struct Envelope *envelope, const struct BodyPart *structure, struct Data body ) { char *path = exportPath(uid, "atom"); FILE *file = fopen(path, "w"); if (!file) err(EX_CANTCREAT, "%s", path); int error = atomEntryOpen(file, envelope); if (error) err(EX_IOERR, "%s", path); const struct BodyPart *part = structure; while (part->multipart) { if (bodyPartType(part, "multipart", "alternative")) { for (size_t i = part->parts.len - 1; i < part->parts.len; --i) { if (!isInline(&part->parts.ptr[i])) continue; part = &part->parts.ptr[i]; body = dataCheck(body, List).list.ptr[i]; break; } if (part->multipart) break; } else { part = &part->parts.ptr[0]; body = dataCheck(body, List).list.ptr[0]; } } if (isInline(part)) { char *content = decodeToString(part, dataCheck(body, String).string); error = atomContent(file, content); if (error) err(EX_IOERR, "%s", path); free(content); } error = atomEntryClose(file) || fclose(file); if (error) err(EX_IOERR, "%s", path); free(path); } static char *sectionSpec(struct List section) { char *buf; size_t len; FILE *file = open_memstream(&buf, &len); if (!file) err(EX_OSERR, "open_memstream"); for (size_t i = 0; i < section.len; ++i) { fprintf( file, "%s%" PRIu32, (i ? "." : ""), dataCheck(section.ptr[i], Number).number ); } int error = fclose(file); if (error) err(EX_OSERR, "open_memstream"); return buf; } static int exportHTMLAttachment( FILE *file, const struct Envelope *envelope, struct List section, const struct BodyPart *part, struct Data body ) { const char *name = paramGet(part->disposition.params, "filename"); if (!name) name = paramGet(part->params, "name"); const char *disposition = part->disposition.type; if (!disposition) disposition = "INLINE"; char *spec = sectionSpec(section); struct Variable vars[] = { { "messageID", envelope->messageID }, { "section", spec }, { "name", (name ? name : "") }, { "disposition", (name ? "" : disposition) }, { ".", (name ? "" : ".") }, { "subtype", (name ? "" : part->subtype) }, {0}, }; char *path = templateString(PATH_ATTACHMENT, vars, escapePath); for (char *ch = path; (ch = strchr(ch, '/')); ++ch) { *ch = '\0'; int error = mkdir(path, 0775); if (error && errno != EEXIST) err(EX_CANTCREAT, "%s", path); *ch = '/'; } FILE *attachment = fopen(path, "w"); if (!file) err(EX_CANTCREAT, "%s", path); int error = 0 || decodeToFile(attachment, part, dataCheck(body, String).string) || fclose(attachment); if (error) err(EX_IOERR, "%s", path); if (!quiet) printf("%s\n", path); free(path); error = htmlAttachment(file, part, vars); free(spec); return error; } static int exportHTMLBody( FILE *file, const struct Envelope *envelope, struct List *section, const struct BodyPart *part, struct Data body ) { if (bodyPartType(part, "multipart", "alternative")) { for (size_t i = part->parts.len - 1; i < part->parts.len; --i) { if (!isInline(&part->parts.ptr[i])) continue; return exportHTMLBody( file, envelope, section, &part->parts.ptr[i], dataCheck(body, List).list.ptr[i] ); } } if (part->multipart) { int error; bool attached = false; for (size_t i = 0; i < part->parts.len; ++i) { if (attached != isAttachment(&part->parts.ptr[i])) { attached ^= true; error = attached ? htmlAttachmentOpen(file) : htmlAttachmentClose(file); if (error) return error; } struct Data num = { .type = Number, .number = 1 + i }; listPush(section, num); error = exportHTMLBody( file, envelope, section, &part->parts.ptr[i], dataCheck(body, List).list.ptr[i] ); if (error) return error; section->len--; } return (attached ? htmlAttachmentClose(file) : 0); } else if (part->message.structure) { const struct BodyPart *structure = part->message.structure; int error = 0 || htmlMessageOpen(file, part->message.envelope, true) || exportHTMLBody(file, envelope, section, structure, body) || htmlMessageClose(file); return error; } else if (isInline(part)) { char *content = decodeToString(part, dataCheck(body, String).string); int error = htmlInline(file, part, content); free(content); return error; } else { return exportHTMLAttachment(file, envelope, *section, part, body); } } static void exportHTML( uint32_t uid, const struct Envelope *envelope, const struct BodyPart *structure, struct Data body ) { char *path = exportPath(uid, "html"); FILE *file = fopen(path, "w"); if (!file) err(EX_CANTCREAT, "%s", path); int error = htmlMessageOpen(file, envelope, false); if (error) err(EX_IOERR, "%s", path); struct List section = {0}; error = exportHTMLBody(file, envelope, §ion, structure, body); if (error) err(EX_IOERR, "%s", path); listFree(section); error = htmlMessageClose(file) || fclose(file); if (error) err(EX_IOERR, "%s", path); free(path); } static void fetchParts( FILE *imap, struct List *section, const struct BodyPart *structure ) { if (structure->multipart) { for (size_t i = 0; i < structure->parts.len; ++i) { struct Data part = { .type = Number, .number = 1 + i }; listPush(section, part); fetchParts(imap, section, &structure->parts.ptr[i]); section->len--; } } else if ( structure->message.structure && structure->message.structure->multipart ) { fetchParts(imap, section, structure->message.structure); } else { char *spec = sectionSpec(*section); fprintf( imap, " BODY[%s%s]", spec, (structure->message.structure ? ".TEXT" : "") ); free(spec); } } static void checkBodyParts(const struct BodyPart *structure, struct Data body) { if (structure->multipart) { struct List list = dataCheck(body, List).list; if (list.len < structure->parts.len) { errx(EX_PROTOCOL, "missing body parts"); } for (size_t i = 0; i < structure->parts.len; ++i) { checkBodyParts(&structure->parts.ptr[i], list.ptr[i]); } } else if ( structure->message.structure && structure->message.structure->multipart ) { checkBodyParts(structure->message.structure, body); } else if (body.type != String) { errx(EX_PROTOCOL, "missing body part"); } } bool exportData(FILE *imap, enum Atom tag, struct List items) { uint32_t uid = 0; struct Envelope envelope = {0}; struct BodyPart structure = {0}; struct Data bodyHeader = {0}; struct Data bodyText = {0}; struct Data bodyParts = {0}; for (size_t i = 0; i + 1 < items.len; i += 2) { enum Atom name = dataCheck(items.ptr[i], Atom).atom; struct Data data = items.ptr[i + 1]; if (name == AtomUID) { uid = dataCheck(data, Number).number; } else if (name == AtomEnvelope) { parseEnvelope(&envelope, dataCheck(data, List).list); } else if (name == AtomBodyStructure) { parseBodyPart(&structure, dataCheck(data, List).list); } if (name != AtomBody) continue; struct List section = dataCheck(data, List).list; if (!section.len) { errx(EX_PROTOCOL, "missing body data item section"); } i++; if (i + 1 >= items.len) { errx(EX_PROTOCOL, "missing body data item value"); } data = items.ptr[i + 1]; if (section.ptr[0].type == Atom) { name = section.ptr[0].atom; if (name == AtomHeader) { bodyHeader = data; } else if (name == AtomText) { bodyText = data; } continue; } struct Data *dest = &bodyParts; for (size_t i = 0; i < section.len; ++i) { if (section.ptr[i].type != Number) continue; uint32_t num = section.ptr[i].number; if (dest->type != List) { *dest = (struct Data) { .type = List }; } while (dest->list.len < num) { listPush(&dest->list, (struct Data) {0}); } dest = &dest->list.ptr[num - 1]; } // Free with bodyParts: *dest = dataTake(&items.ptr[i + 1]); } if (!uid) { errx(EX_PROTOCOL, "missing UID data item"); } if (!envelope.subject) { errx(EX_PROTOCOL, "missing ENVELOPE data item"); } if (!structure.subtype) { errx(EX_PROTOCOL, "missing BODYSTRUCTURE data item"); } if (bodyParts.type == List) { checkBodyParts(&structure, bodyParts); } if (bodyHeader.type == String && bodyText.type == String) { exportMbox(uid, &envelope, bodyHeader.string, bodyText.string); } bool fetch = false; if (!structure.multipart) { exportAtom(uid, &envelope, &structure, bodyText); exportHTML(uid, &envelope, &structure, bodyText); } else if (bodyParts.type == List) { exportAtom(uid, &envelope, &structure, bodyParts); exportHTML(uid, &envelope, &structure, bodyParts); } else { fetch = true; fprintf( imap, "%s UID FETCH %" PRIu32 " (UID ENVELOPE BODYSTRUCTURE", Atoms[tag], uid ); struct List section = {0}; fetchParts(imap, §ion, &structure); listFree(section); fprintf(imap, ")\r\n"); } envelopeFree(envelope); bodyPartFree(structure); dataFree(bodyParts); return fetch; }